diff --git a/doc/development/deprecations.rst b/doc/development/deprecations.rst index d37928ce21f..4d4308c39fb 100644 --- a/doc/development/deprecations.rst +++ b/doc/development/deprecations.rst @@ -9,12 +9,6 @@ deprecations are listed below. Pending deprecations -------------------- -* The ``qsvt_legacy`` function has been deprecated. - Instead, use ``qml.qsvt``. The new functionality takes an input polynomial instead of angles. - - - Deprecated in v0.40 - - Will be removed in v0.41 - * The ``tape`` and ``qtape`` properties of ``QNode`` have been deprecated. Instead, use the ``qml.workflow.construct_tape`` function. @@ -31,11 +25,6 @@ Pending deprecations - Deprecated in v0.40 - Will be removed in v0.41 -* The ``output_dim`` property of ``qml.tape.QuantumScript`` has been deprecated. Instead, use method ``shape`` of ``QuantumScript`` or ``MeasurementProcess`` to get the same information. - - - Deprecated in v0.40 - - Will be removed in v0.41 - * The ``gradient_fn`` keyword argument to ``qml.execute`` has been renamed ``diff_method``. - Deprecated in v0.40 @@ -86,6 +75,17 @@ Completed deprecation cycles * The ``QNode.get_best_method`` and ``QNode.best_method_str`` methods have been removed. Instead, use the ``qml.workflow.get_best_diff_method`` function. + + - Deprecated in v0.40 + - Removed in v0.41 + +* The ``output_dim`` property of ``qml.tape.QuantumScript`` has been removed. Instead, use method ``shape`` of ``QuantumScript`` or ``MeasurementProcess`` to get the same information. + + - Deprecated in v0.40 + - Removed in v0.41 + +* The ``qml.qsvt_legacy`` function has been removed. + Instead, use ``qml.qsvt``. The new functionality takes an input polynomial instead of angles. - Deprecated in v0.40 - Removed in v0.41 diff --git a/doc/releases/changelog-0.40.0.md b/doc/releases/changelog-0.40.0.md index 39e5b6ecc92..bb9cb7cd3bc 100644 --- a/doc/releases/changelog-0.40.0.md +++ b/doc/releases/changelog-0.40.0.md @@ -1170,4 +1170,4 @@ Anton Naim Ibrahim, Andrija Paurevic, Justin Pickering, Jay Soni, -David Wierichs. +David Wierichs. \ No newline at end of file diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 1466d686d86..1810651f858 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -6,12 +6,21 @@

Improvements 🛠

+* The coefficients of observables now have improved differentiability. + [(#6598)](https://github.com/PennyLaneAI/pennylane/pull/6598) +

Breaking changes 💔

* The ``QNode.get_best_method`` and ``QNode.best_method_str`` methods have been removed. Instead, use the ``qml.workflow.get_best_diff_method`` function. [(#6823)](https://github.com/PennyLaneAI/pennylane/pull/6823) +* The `output_dim` property of `qml.tape.QuantumScript` has been removed. Instead, use method `shape` of `QuantumScript` or `MeasurementProcess` to get the same information. + [(#6829)](https://github.com/PennyLaneAI/pennylane/pull/6829) + +* Removed method `qsvt_legacy` along with its private helper `_qsp_to_qsvt` + [(#6827)](https://github.com/PennyLaneAI/pennylane/pull/6827) +

Deprecations 👋

Documentation 📝

@@ -24,4 +33,7 @@

Contributors ✍️

This release contains contributions from (in alphabetical order): -Diksha Dhawan + +Yushao Chen, +Diksha Dhawan, +Christina Lee, diff --git a/pennylane/_version.py b/pennylane/_version.py index e13f6bf4d72..1b991cac3ad 100644 --- a/pennylane/_version.py +++ b/pennylane/_version.py @@ -16,4 +16,4 @@ Version number (major.minor.patch[-label]) """ -__version__ = "0.41.0-dev6" +__version__ = "0.41.0-dev7" diff --git a/pennylane/compiler/compiler.py b/pennylane/compiler/compiler.py index cbb38243d83..6c38bb3d203 100644 --- a/pennylane/compiler/compiler.py +++ b/pennylane/compiler/compiler.py @@ -23,7 +23,7 @@ from packaging.version import Version -PL_CATALYST_MIN_VERSION = Version("0.9.0") +PL_CATALYST_MIN_VERSION = Version("0.10.0") class CompileError(Exception): diff --git a/pennylane/devices/_legacy_device.py b/pennylane/devices/_legacy_device.py index 33400c37695..b4f37a654b1 100644 --- a/pennylane/devices/_legacy_device.py +++ b/pennylane/devices/_legacy_device.py @@ -93,7 +93,6 @@ def _local_tape_expand(tape, depth, stop_at): # Update circuit info new_tape._batch_size = tape._batch_size - new_tape._output_dim = tape._output_dim return new_tape diff --git a/pennylane/gradients/parameter_shift.py b/pennylane/gradients/parameter_shift.py index 5c25237db58..6fcdd17df19 100644 --- a/pennylane/gradients/parameter_shift.py +++ b/pennylane/gradients/parameter_shift.py @@ -752,6 +752,12 @@ def _param_shift_stopping_condition(op) -> bool: return True +def _inplace_set_trainable_params(tape): + """Update all the trainable params in place.""" + params = tape.get_parameters(trainable_only=False) + tape.trainable_params = qml.math.get_trainable_indices(params) + + def _expand_transform_param_shift( tape: QuantumScript, argnum=None, @@ -769,11 +775,21 @@ def _expand_transform_param_shift( name="param_shift", error=qml.operation.DecompositionUndefinedError, ) - if new_tape is tape: - return [tape], postprocessing - params = new_tape.get_parameters(trainable_only=False) - new_tape.trainable_params = qml.math.get_trainable_indices(params) - return [new_tape], postprocessing + if any( + qml.math.requires_grad(d) for mp in tape.measurements for d in getattr(mp.obs, "data", []) + ): + try: + batch, postprocessing = qml.transforms.split_to_single_terms(new_tape) + except RuntimeError as e: + raise ValueError( + "Can only differentiate Hamiltonian " + f"coefficients for expectations, not {tape.measurements}." + ) from e + else: + batch = [new_tape] + if len(batch) > 1 or batch[0] is not tape: + _ = [_inplace_set_trainable_params(t) for t in batch] + return batch, postprocessing @partial( diff --git a/pennylane/ops/functions/dot.py b/pennylane/ops/functions/dot.py index de4c2f76225..e2e2d11a470 100644 --- a/pennylane/ops/functions/dot.py +++ b/pennylane/ops/functions/dot.py @@ -142,10 +142,12 @@ def dot( f"ops must be an Iterable of {t.__name__}'s, not a {t.__name__} itself." ) - if len(coeffs) != len(ops): - raise ValueError("Number of coefficients and operators does not match.") - if len(coeffs) == 0 and len(ops) == 0: - raise ValueError("Cannot compute the dot product of an empty sequence.") + # tensorflow variables have no len + if qml.math.get_interface(coeffs) != "tensorflow": + if len(coeffs) != len(ops): + raise ValueError("Number of coefficients and operators does not match.") + if len(coeffs) == 0 and len(ops) == 0: + raise ValueError("Cannot compute the dot product of an empty sequence.") for t in (Operator, PauliWord, PauliSentence): if isinstance(ops, t): diff --git a/pennylane/ops/op_math/sprod.py b/pennylane/ops/op_math/sprod.py index bd8e6a3a3e5..eca3bee5cf9 100644 --- a/pennylane/ops/op_math/sprod.py +++ b/pennylane/ops/op_math/sprod.py @@ -15,7 +15,6 @@ This file contains the implementation of the SProd class which contains logic for computing the scalar product of operations. """ -from copy import copy from typing import Union import pennylane as qml @@ -148,7 +147,6 @@ def __init__( elif (base_pauli_rep := getattr(self.base, "pauli_rep", None)) and ( self.batch_size is None ): - scalar = copy(self.scalar) pr = {pw: qnp.dot(coeff, scalar) for pw, coeff in base_pauli_rep.items()} self._pauli_rep = qml.pauli.PauliSentence(pr) diff --git a/pennylane/tape/qscript.py b/pennylane/tape/qscript.py index 38cdf5ff6bc..f7f0bd2c918 100644 --- a/pennylane/tape/qscript.py +++ b/pennylane/tape/qscript.py @@ -25,7 +25,7 @@ from typing import Any, Optional, TypeVar, Union import pennylane as qml -from pennylane.measurements import MeasurementProcess, ProbabilityMP, StateMP +from pennylane.measurements import MeasurementProcess from pennylane.measurements.shots import Shots, ShotsLike from pennylane.operation import _UNSET_BATCH_SIZE, Observable, Operation, Operator from pennylane.pytrees import register_pytree @@ -183,7 +183,6 @@ def __init__( self._trainable_params = trainable_params self._graph = None self._specs = None - self._output_dim = None self._batch_size = _UNSET_BATCH_SIZE self._obs_sharing_wires = None @@ -322,29 +321,6 @@ def batch_size(self) -> Optional[int]: self._update_batch_size() return self._batch_size - @property - def output_dim(self) -> int: - """The (inferred) output dimension of the quantum script. - - .. warning:: - - ``QuantumScript.output_dim`` is being deprecated. Instead, considering - using method ``shape`` of ``QuantumScript`` or ``MeasurementProcess`` - to get the same information. See ``qml.gradients.parameter_shift_cv.py::_get_output_dim`` - for an example. - - """ - # pylint: disable=import-outside-toplevel - import warnings - - warnings.warn( - "The 'output_dim' property is deprecated and will be removed in version 0.41", - qml.PennyLaneDeprecationWarning, - ) - if self._output_dim is None: - self._update_output_dim() # this will set _batch_size if it isn't already - return self._output_dim - @property def diagonalizing_gates(self) -> list[Operation]: """Returns the gates that diagonalize the measured wires such that they @@ -566,28 +542,6 @@ def _update_batch_size(self): self._batch_size = candidate - def _update_output_dim(self): - """Update the dimension of the output of the quantum script. - - Sets: - self._output_dim (int): Size of the quantum script output (when flattened) - - This method makes use of `self.batch_size`, so that `self._batch_size` - needs to be up to date when calling it. - Call `_update_batch_size` before `_update_output_dim` - """ - self._output_dim = 0 - for m in self.measurements: - # attempt to infer the output dimension - if isinstance(m, ProbabilityMP): - # TODO: what if we had a CV device here? Having the base as - # 2 would have to be swapped to the cutoff value - self._output_dim += 2 ** len(m.wires) - elif not isinstance(m, StateMP): - self._output_dim += 1 - if self.batch_size: - self._output_dim *= self.batch_size - # ======================================================== # Parameter handling # ======================================================== @@ -984,9 +938,6 @@ def copy(self, copy_operations: bool = False, **update) -> "QuantumScript": # obs may change if measurements were updated new_qscript._obs_sharing_wires = self._obs_sharing_wires new_qscript._obs_sharing_wires_id = self._obs_sharing_wires_id - if not (update.get("measurements") or update.get("operations")): - # output_dim may change if either measurements or operations were updated - new_qscript._output_dim = self._output_dim return new_qscript def __copy__(self) -> "QuantumScript": diff --git a/pennylane/tape/tape.py b/pennylane/tape/tape.py index 4d08187b147..80bff4c29e5 100644 --- a/pennylane/tape/tape.py +++ b/pennylane/tape/tape.py @@ -294,7 +294,6 @@ def stop_at(obj): # pylint: disable=unused-argument # Update circuit info new_tape._batch_size = tape._batch_size - new_tape._output_dim = tape._output_dim return new_tape @@ -343,7 +342,6 @@ def expand_tape_state_prep(tape, skip_first=True): # Update circuit info new_tape._batch_size = tape._batch_size - new_tape._output_dim = tape._output_dim return new_tape diff --git a/pennylane/templates/subroutines/__init__.py b/pennylane/templates/subroutines/__init__.py index 517c86fe003..b3960b17341 100644 --- a/pennylane/templates/subroutines/__init__.py +++ b/pennylane/templates/subroutines/__init__.py @@ -33,7 +33,7 @@ from .hilbert_schmidt import HilbertSchmidt, LocalHilbertSchmidt from .flip_sign import FlipSign from .basis_rotation import BasisRotation -from .qsvt import poly_to_angles, QSVT, qsvt, qsvt_legacy, transform_angles +from .qsvt import poly_to_angles, QSVT, qsvt, transform_angles from .select import Select from .qdrift import QDrift from .controlled_sequence import ControlledSequence diff --git a/pennylane/templates/subroutines/qsvt.py b/pennylane/templates/subroutines/qsvt.py index 129e6d930a1..2898de207f0 100644 --- a/pennylane/templates/subroutines/qsvt.py +++ b/pennylane/templates/subroutines/qsvt.py @@ -16,7 +16,6 @@ """ # pylint: disable=too-many-arguments import copy -import warnings from typing import Literal import numpy as np @@ -24,147 +23,12 @@ import pennylane as qml from pennylane.operation import AnyWires, Operation -from pennylane.ops import BlockEncode, PCPhase from pennylane.ops.op_math import adjoint from pennylane.queuing import QueuingManager from pennylane.wires import Wires -def qsvt_legacy(A, angles, wires, convention=None): - r"""Implements the - `quantum singular value transformation `__ (QSVT) circuit. - - .. warning:: - - The ``qsvt_legacy`` function has been deprecated. - Instead, use :func:`~pennylane.qsvt`. The new functionality takes an input polynomial instead of angles. - - .. note :: - - :class:`~.BlockEncode` and :class:`~.PCPhase` used in this implementation of QSVT - are matrix-based operators and well-suited for simulators. - To implement QSVT with user-defined circuits for the block encoding and - projector-controlled phase shifts, use the :class:`~.QSVT` template. - - Given a matrix :math:`A`, and a list of angles :math:`\vec{\phi}`, this function applies a - circuit for the quantum singular value transformation using :class:`~.BlockEncode` and - :class:`~.PCPhase`. - - When the number of angles is even (:math:`d` is odd), the QSVT circuit is defined as: - - .. math:: - - U_{QSVT} = \tilde{\Pi}_{\phi_1}U\left[\prod^{(d-1)/2}_{k=1}\Pi_{\phi_{2k}}U^\dagger - \tilde{\Pi}_{\phi_{2k+1}}U\right]\Pi_{\phi_{d+1}}, - - - and when the number of angles is odd (:math:`d` is even): - - .. math:: - - U_{QSVT} = \left[\prod^{d/2}_{k=1}\Pi_{\phi_{2k-1}}U^\dagger\tilde{\Pi}_{\phi_{2k}}U\right] - \Pi_{\phi_{d+1}}. - - Here, :math:`U` denotes a block encoding of :math:`A` via :class:`~.BlockEncode` and - :math:`\Pi_\phi` denotes a projector-controlled phase shift with angle :math:`\phi` - via :class:`~.PCPhase`. - - This circuit applies a polynomial transformation (:math:`Poly^{SV}`) to the singular values of - the block encoded matrix: - - .. math:: - - \begin{align} - U_{QSVT}(A, \phi) &= - \begin{bmatrix} - Poly^{SV}(A) & \cdot \\ - \cdot & \cdot - \end{bmatrix}. - \end{align} - - The polynomial transformation is determined by a combination of the block encoding and choice of angles, - :math:`\vec{\phi}`. The convention used by :class:`~.BlockEncode` is commonly refered to as the - reflection convention or :math:`R` convention. Another equivalent convention for the block encoding is - the :math:`Wx` or rotation convention. - - Depending on the choice of convention for blockencoding, the same phase angles will produce different - polynomial transformations. We provide the functionality to swap between blockencoding conventions and - to transform the phase angles accordingly using the :code:`convention` keyword argument. - - .. seealso:: - - :class:`~.QSVT` and `A Grand Unification of Quantum Algorithms `_. - - Args: - A (tensor_like): the general :math:`(n \times m)` matrix to be encoded - angles (tensor_like): a list of angles by which to shift to obtain the desired polynomial - wires (Iterable[int, str], Wires): the wires the template acts on - convention (string): can be set to ``"Wx"`` to convert quantum signal processing angles in the - `Wx` convention to QSVT angles. - - **Example** - - To implement QSVT in a circuit, we can use the following method: - - >>> dev = qml.device("default.qubit", wires=2) - >>> A = np.array([[0.1, 0.2], [0.3, 0.4]]) - >>> angles = np.array([0.1, 0.2, 0.3]) - >>> @qml.qnode(dev) - ... def example_circuit(A): - ... qml.qsvt_legacy(A, angles, wires=[0, 1]) - ... return qml.expval(qml.Z(0)) - - The resulting circuit implements QSVT. - - >>> print(qml.draw(example_circuit)(A)) - 0: ─╭QSVT─┤ - 1: ─╰QSVT─┤ - - To see the implementation details, we can expand the circuit: - - >>> q_script = qml.tape.QuantumScript(ops=[qml.qsvt_legacy(A, angles, wires=[0, 1])]) - >>> print(q_script.expand().draw(decimals=2)) - 0: ─╭∏_ϕ(0.30)─╭BlockEncode(M0)─╭∏_ϕ(0.20)─╭BlockEncode(M0)†─╭∏_ϕ(0.10)─┤ - 1: ─╰∏_ϕ(0.30)─╰BlockEncode(M0)─╰∏_ϕ(0.20)─╰BlockEncode(M0)†─╰∏_ϕ(0.10)─┤ - """ - - warnings.warn( - "`qml.qsvt_legacy` is deprecated and will be removed in version 0.41. " - "Instead, please use `qml.qsvt` functionality.", - qml.PennyLaneDeprecationWarning, - ) - - if qml.math.shape(A) == () or qml.math.shape(A) == (1,): - A = qml.math.reshape(A, [1, 1]) - - c, r = qml.math.shape(A) - global_phase, global_phase_op = None, None - - with qml.QueuingManager.stop_recording(): - UA = BlockEncode(A, wires=wires) - projectors = [] - - if convention == "Wx": - angles = _qsp_to_qsvt(angles) - global_phase = (len(angles) - 1) % 4 - - if global_phase: - with qml.QueuingManager.stop_recording(): - global_phase_op = qml.GlobalPhase(-0.5 * np.pi * (4 - global_phase), wires=wires) - - for idx, phi in enumerate(angles): - dim = c if idx % 2 else r - with qml.QueuingManager.stop_recording(): - projectors.append(PCPhase(phi, dim=dim, wires=wires)) - - projectors = projectors[::-1] # reverse order to match equation - - if convention == "Wx" and global_phase: - return qml.prod(global_phase_op, QSVT(UA, projectors)) - return QSVT(UA, projectors) - - -# pylint: disable=too-many-branches +# pylint: disable=too-many-branches, unused-argument def qsvt(A, poly, encoding_wires=None, block_encoding=None, **kwargs): r""" Implements the Quantum Singular Value Transformation (QSVT) for a matrix or Hamiltonian ``A``, @@ -329,14 +193,6 @@ def circuit(): """ - if encoding_wires is None or block_encoding is None or "wires" in kwargs: - warnings.warn( - "You may be trying to use the old `qsvt` functionality (now `qml.qsvt_legacy`).\n" - "Make sure you pass a polynomial instead of angles.\n" - "Set a value for `block_encoding` to silence this warning.\n", - qml.PennyLaneDeprecationWarning, - ) - projectors = [] # If the input A is a Hamiltonian @@ -768,19 +624,6 @@ def compute_matrix(*args, **kwargs): return mat -def _qsp_to_qsvt(angles): - r"""Converts qsp angles to qsvt angles.""" - num_angles = len(angles) - update_vals = np.empty(num_angles) - - update_vals[0] = 3 * np.pi / 4 - update_vals[1:-1] = np.pi / 2 - update_vals[-1] = -np.pi / 4 - update_vals = qml.math.convert_like(update_vals, angles) - - return angles + update_vals - - def _complementary_poly(poly_coeffs): r""" Computes the complementary polynomial Q given a polynomial P. diff --git a/pennylane/transforms/split_non_commuting.py b/pennylane/transforms/split_non_commuting.py index 1faab45de73..ba282962233 100644 --- a/pennylane/transforms/split_non_commuting.py +++ b/pennylane/transforms/split_non_commuting.py @@ -18,11 +18,11 @@ # pylint: disable=too-many-arguments,too-many-boolean-expressions -from functools import partial +from functools import partial, wraps from typing import Optional import pennylane as qml -from pennylane.measurements import ExpectationMP, MeasurementProcess, Shots, StateMP +from pennylane.measurements import ExpectationMP, MeasurementProcess, StateMP from pennylane.ops import Prod, SProd, Sum from pennylane.tape import QuantumScript, QuantumScriptBatch from pennylane.transforms import transform @@ -36,6 +36,16 @@ def null_postprocessing(results): return results[0] +def shot_vector_support(initial_postprocessing: PostprocessingFn) -> PostprocessingFn: + """Convert a postprocessing function to one with shot vector support.""" + + @wraps(initial_postprocessing) + def shot_vector_postprocessing(results): + return tuple(initial_postprocessing(r) for r in zip(*results)) + + return shot_vector_postprocessing + + @transform def split_non_commuting( tape: QuantumScript, grouping_strategy: Optional[str] = "default" @@ -280,13 +290,15 @@ def circuit(x): if grouping_strategy is None: measurements = list(single_term_obs_mps.keys()) tapes = [tape.copy(measurements=[m]) for m in measurements] - return tapes, partial( + fn = partial( _processing_fn_no_grouping, single_term_obs_mps=single_term_obs_mps, offsets=offsets, - shots=tape.shots, batch_size=tape.batch_size, ) + if tape.shots.has_partitioned_shots: + fn = shot_vector_support(fn) + return tapes, fn if grouping_strategy == "wires" or any( m.obs is not None and not qml.pauli.is_pauli_word(m.obs) for m in single_term_obs_mps @@ -360,14 +372,16 @@ def _split_ham_with_grouping(tape: qml.tape.QuantumScript): group_sizes.append(group_size) tapes = [tape.copy(measurements=mps) for mps in mp_groups] - return tapes, partial( + fn = partial( _processing_fn_with_grouping, single_term_obs_mps=single_term_obs_mps, offsets=[offset], group_sizes=group_sizes, - shots=tape.shots, batch_size=tape.batch_size, ) + if tape.shots.has_partitioned_shots: + fn = shot_vector_support(fn) + return tapes, fn def _split_using_qwc_grouping( @@ -424,16 +438,17 @@ def _split_using_qwc_grouping( 0, ) group_sizes.append(1) - tapes = [tape.copy(measurements=mps) for mps in mp_groups] - return tapes, partial( + fn = partial( _processing_fn_with_grouping, single_term_obs_mps=single_term_obs_mps_grouped, offsets=offsets, group_sizes=group_sizes, - shots=tape.shots, batch_size=tape.batch_size, ) + if tape.shots.has_partitioned_shots: + fn = shot_vector_support(fn) + return tapes, fn def _split_using_wires_grouping( @@ -497,14 +512,16 @@ def _split_using_wires_grouping( num_groups += 1 tapes = [tape.copy(measurements=mps) for mps in mp_groups] - return tapes, partial( + fn = partial( _processing_fn_with_grouping, single_term_obs_mps=single_term_obs_mps_grouped, offsets=offsets, group_sizes=group_sizes, - shots=tape.shots, batch_size=tape.batch_size, ) + if tape.shots.has_partitioned_shots: + fn = shot_vector_support(fn) + return tapes, fn def _split_all_multi_term_obs_mps(tape: qml.tape.QuantumScript): @@ -572,8 +589,7 @@ def _processing_fn_no_grouping( res: ResultBatch, single_term_obs_mps: dict[MeasurementProcess, tuple[list[int], list[Union[float, TensorLike]]]], offsets: list[Union[float, TensorLike]], - shots: Shots, - batch_size: int, + batch_size: Union[None, int], ): """Postprocessing function for the split_non_commuting transform without grouping. @@ -592,12 +608,22 @@ def _processing_fn_no_grouping( coeffs_for_each_mp = [[] for _ in offsets] for smp_idx, (_, (mp_indices, coeffs)) in enumerate(single_term_obs_mps.items()): - for mp_idx, coeff in zip(mp_indices, coeffs): res_batch_for_each_mp[mp_idx].append(res[smp_idx]) coeffs_for_each_mp[mp_idx].append(coeff) - return _res_for_each_mp(res_batch_for_each_mp, coeffs_for_each_mp, offsets, shots, batch_size) + result_shape = (batch_size,) if batch_size and batch_size > 1 else () + # Sum up the results for each original measurement + + res_for_each_mp = [ + _sum_terms(_sub_res, coeffs, offset, result_shape) + for _sub_res, coeffs, offset in zip(res_batch_for_each_mp, coeffs_for_each_mp, offsets) + ] + # res_for_each_mp should have shape (n_mps, [,n_shots] [,batch_size]) + if len(res_for_each_mp) == 1: + return res_for_each_mp[0] + + return tuple(res_for_each_mp) def _processing_fn_with_grouping( @@ -605,9 +631,8 @@ def _processing_fn_with_grouping( single_term_obs_mps: dict[ MeasurementProcess, tuple[list[int], list[Union[float, TensorLike]], int, int] ], - offsets: list[Union[float, TensorLike]], + offsets: list[TensorLike], group_sizes: list[int], - shots: Shots, batch_size: int, ): """Postprocessing function for the split_non_commuting transform with grouping. @@ -636,26 +661,16 @@ def _processing_fn_with_grouping( res_group = res[group_idx] # ([n_shots] [,n_mps] [,batch_size]) group_size = group_sizes[group_idx] - if group_size > 1 and shots.has_partitioned_shots: - # Each result should have shape ([n_shots] [,batch_size]) - sub_res = [_res[mp_idx_in_group] for _res in res_group] - else: - # If there is only one term in the group, the n_mps dimension would have - # been squeezed out, use the entire result directly. - sub_res = res_group if group_size == 1 else res_group[mp_idx_in_group] + # If there is only one term in the group, the n_mps dimension would have + # been squeezed out, use the entire result directly. + sub_res = res_group if group_size == 1 else res_group[mp_idx_in_group] # Add this result to the result batch for the corresponding original measurement for mp_idx, coeff in zip(mp_indices, coeffs): res_batch_for_each_mp[mp_idx].append(sub_res) coeffs_for_each_mp[mp_idx].append(coeff) - return _res_for_each_mp(res_batch_for_each_mp, coeffs_for_each_mp, offsets, shots, batch_size) - - -def _res_for_each_mp(res_batch_for_each_mp, coeffs_for_each_mp, offsets, shots, batch_size): - """Helper function that combines a result batch into results for each mp""" - - result_shape = _infer_result_shape(shots, batch_size) + result_shape = (batch_size,) if batch_size and batch_size > 1 else () # Sum up the results for each original measurement res_for_each_mp = [ @@ -667,14 +682,6 @@ def _res_for_each_mp(res_batch_for_each_mp, coeffs_for_each_mp, offsets, shots, if len(res_for_each_mp) == 1: return res_for_each_mp[0] - if shots.has_partitioned_shots: - # If the shot vector dimension exists, it should be moved to the first axis - # Basically, the shape becomes (n_shots, n_mps, [,batch_size]) - res_for_each_mp = [ - tuple(res_for_each_mp[j][i] for j in range(len(res_for_each_mp))) - for i in range(shots.num_copies) - ] - return tuple(res_for_each_mp) @@ -685,9 +692,13 @@ def _sum_terms( shape: tuple, ) -> Result: """Sum results from measurements of multiple terms in a multi-term observable.""" - - # Trivially return the original result - if coeffs == [1] and offset == 0: + if ( + coeffs + and not qml.math.is_abstract(coeffs[0]) + and not qml.math.is_abstract(offset) + and coeffs == [1] + and offset == 0 + ): return res[0] # The shape of res at this point is (n_terms, [,n_shots] [,batch_size]) @@ -695,10 +706,11 @@ def _sum_terms( for c, r in zip(coeffs, res): if qml.math.get_interface(r) == "autograd": r = qml.math.array(r) - dot_products.append(qml.math.dot(qml.math.squeeze(r), c)) + if isinstance(r, (list, tuple)): + r = qml.math.stack(r) + dot_products.append(qml.math.dot(c, qml.math.squeeze(r))) if len(dot_products) == 0: return qml.math.ones(shape) * offset - summed_dot_products = qml.math.sum(qml.math.stack(dot_products), axis=0) if qml.math.get_interface(offset) == "autograd" and qml.math.requires_grad(summed_dot_products): offset = qml.math.array(offset) @@ -718,14 +730,3 @@ def _mp_to_obs(mp: MeasurementProcess, tape: qml.tape.QuantumScript) -> qml.oper obs_wires = mp.wires if mp.wires else tape.wires return qml.prod(*(qml.Z(wire) for wire in obs_wires)) - - -def _infer_result_shape(shots: Shots, batch_size: int) -> tuple: - """Based on the result, infer the ([,n_shots] [,batch_size]) shape of the result.""" - - shape = () - if shots.has_partitioned_shots: - shape += (shots.num_copies,) - if batch_size and batch_size > 1: - shape += (batch_size,) - return shape diff --git a/pennylane/transforms/split_to_single_terms.py b/pennylane/transforms/split_to_single_terms.py index 3bbe2cbb0f9..f794d8424ec 100644 --- a/pennylane/transforms/split_to_single_terms.py +++ b/pennylane/transforms/split_to_single_terms.py @@ -24,6 +24,7 @@ from pennylane.transforms.split_non_commuting import ( _processing_fn_no_grouping, _split_all_multi_term_obs_mps, + shot_vector_support, ) @@ -162,23 +163,13 @@ def post_processing_split_sums(res): _processing_fn_no_grouping, single_term_obs_mps=single_term_obs_mps, offsets=offsets, - shots=tape.shots, batch_size=tape.batch_size, ) - if len(new_tape.measurements) == 1: - return process(res) - # we go from ((mp1_res, mp2_res, mp3_res),) as result output # to (mp1_res, mp2_res, mp3_res) as expected by _processing_fn_no_grouping - res = res[0] - if tape.shots.has_partitioned_shots: - # swap dimension order of mps vs shot copies for _processing_fn_no_grouping - res = [ - tuple(res[j][i] for j in range(tape.shots.num_copies)) - for i in range(len(new_tape.measurements)) - ] - - return process(res) + return process(res if len(new_tape.measurements) == 1 else res[0]) + if tape.shots.has_partitioned_shots: + return (new_tape,), shot_vector_support(post_processing_split_sums) return (new_tape,), post_processing_split_sums diff --git a/pennylane/workflow/execution.py b/pennylane/workflow/execution.py index 7c9fb93b001..74fe6949c96 100644 --- a/pennylane/workflow/execution.py +++ b/pennylane/workflow/execution.py @@ -44,6 +44,7 @@ def execute( device: Union["qml.devices.LegacyDevice", "qml.devices.Device"], diff_method: Optional[Union[Callable, str, qml.transforms.core.TransformDispatcher]] = None, interface: Optional[Union[str, Interface]] = Interface.AUTO, + *, transform_program=None, inner_transform=None, config=None, diff --git a/setup.py b/setup.py index a6ce2fb7a08..47d6484df65 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ "appdirs", "autoray>=0.6.11", "cachetools", - "pennylane-lightning>=0.39", + "pennylane-lightning>=0.40", "requests", "typing_extensions", "packaging", diff --git a/tests/gradients/parameter_shift/test_parameter_shift.py b/tests/gradients/parameter_shift/test_parameter_shift.py index 499fbdf2ad5..f35a4b4fc95 100644 --- a/tests/gradients/parameter_shift/test_parameter_shift.py +++ b/tests/gradients/parameter_shift/test_parameter_shift.py @@ -3363,14 +3363,14 @@ def test_not_expval_error(self, broadcast): tape = qml.tape.QuantumScript.from_queue(q) tape.trainable_params = {2, 3, 4} - with pytest.raises(ValueError, match="for expectations, not var"): + with pytest.raises(ValueError, match="for expectations, not"): qml.gradients.param_shift(tape, broadcast=broadcast) def test_not_expval_pass_if_not_trainable_hamiltonian(self, broadcast): """Test that if the variance of a non-trainable Hamiltonian is requested, no error is raised""" obs = [qml.PauliZ(0), qml.PauliZ(0) @ qml.PauliX(1), qml.PauliY(0)] - coeffs = np.array([0.1, 0.2, 0.3]) + coeffs = np.array([0.1, 0.2, 0.3], requires_grad=False) H = qml.Hamiltonian(coeffs, obs) weights = np.array([0.4, 0.5]) @@ -3393,7 +3393,7 @@ def test_no_trainable_coeffs(self, mocker, tol, broadcast): spy = mocker.spy(qml.gradients, "hamiltonian_grad") obs = [qml.PauliZ(0), qml.PauliZ(0) @ qml.PauliX(1), qml.PauliY(0)] - coeffs = np.array([0.1, 0.2, 0.3]) + coeffs = np.array([0.1, 0.2, 0.3], requires_grad=False) H = qml.Hamiltonian(coeffs, obs) weights = np.array([0.4, 0.5]) @@ -3433,10 +3433,9 @@ def test_no_trainable_coeffs(self, mocker, tol, broadcast): assert np.allclose(res[0], expected[0], atol=tol, rtol=0) assert np.allclose(res[1], expected[1], atol=tol, rtol=0) - def test_trainable_coeffs(self, mocker, tol, broadcast): + def test_trainable_coeffs(self, tol, broadcast): """Test trainable Hamiltonian coefficients""" dev = qml.device("default.qubit", wires=2) - spy = mocker.spy(qml.gradients, "hamiltonian_grad") obs = [qml.PauliZ(0), qml.PauliZ(0) @ qml.PauliX(1), qml.PauliY(0)] coeffs = np.array([0.1, 0.2, 0.3]) @@ -3462,33 +3461,21 @@ def test_trainable_coeffs(self, mocker, tol, broadcast): tapes, fn = qml.gradients.param_shift(tape, broadcast=broadcast) # two (broadcasted if broadcast=True) shifts per rotation gate # one circuit per trainable H term - assert len(tapes) == (2 + 2 if broadcast else 2 * 2 + 2) - assert [t.batch_size for t in tapes] == ([2, 2, None, None] if broadcast else [None] * 6) - spy.assert_called() + assert len(tapes) == (2 if broadcast else 2 * 2) + assert [t.batch_size for t in tapes] == ([2, 2] if broadcast else [None] * 4) res = fn(dev.execute(tapes)) - assert isinstance(res, tuple) - assert len(res) == 4 - assert res[0].shape == () - assert res[1].shape == () - assert res[2].shape == () - assert res[3].shape == () expected = [ -c * np.cos(x) * np.sin(y) - np.sin(x) * (a + b * np.sin(y)), b * np.cos(x) * np.cos(y) - c * np.cos(y) * np.sin(x), - np.cos(x), - -(np.sin(x) * np.sin(y)), ] assert np.allclose(res[0], expected[0], atol=tol, rtol=0) assert np.allclose(res[1], expected[1], atol=tol, rtol=0) - assert np.allclose(res[2], expected[2], atol=tol, rtol=0) - assert np.allclose(res[3], expected[3], atol=tol, rtol=0) - def test_multiple_hamiltonians(self, mocker, tol, broadcast): + def test_multiple_hamiltonians(self, tol, broadcast): """Test multiple trainable Hamiltonian coefficients""" dev = qml.device("default.qubit", wires=2) - spy = mocker.spy(qml.gradients, "hamiltonian_grad") obs = [qml.PauliZ(0), qml.PauliZ(0) @ qml.PauliX(1), qml.PauliY(0)] coeffs = np.array([0.1, 0.2, 0.3]) @@ -3520,24 +3507,20 @@ def test_multiple_hamiltonians(self, mocker, tol, broadcast): tapes, fn = qml.gradients.param_shift(tape, broadcast=broadcast) # two shifts per rotation gate (in one batched tape if broadcasting), # one circuit per trainable H term - assert len(tapes) == 2 * (1 if broadcast else 2) + 3 - spy.assert_called() + assert len(tapes) == 2 * (1 if broadcast else 2) res = fn(dev.execute(tapes)) assert isinstance(res, tuple) assert len(res) == 2 - assert len(res[0]) == 5 - assert len(res[1]) == 5 + assert len(res[0]) == 2 + assert len(res[1]) == 2 expected = [ [ -c * np.cos(x) * np.sin(y) - np.sin(x) * (a + b * np.sin(y)), b * np.cos(x) * np.cos(y) - c * np.cos(y) * np.sin(x), - np.cos(x), - -(np.sin(x) * np.sin(y)), - 0, ], - [-d * np.sin(x), 0, 0, 0, np.cos(x)], + [-d * np.sin(x), 0], ] assert np.allclose(np.stack(res), expected, atol=tol, rtol=0) @@ -3574,12 +3557,8 @@ def cost_fn_expected(weights, coeffs1, coeffs2): [ -c * np.cos(x) * np.sin(y) - np.sin(x) * (a + b * np.sin(y)), b * np.cos(x) * np.cos(y) - c * np.cos(y) * np.sin(x), - np.cos(x), - np.cos(x) * np.sin(y), - -(np.sin(x) * np.sin(y)), - 0, ], - [-d * np.sin(x), 0, 0, 0, 0, np.cos(x)], + [-d * np.sin(x), 0], ] @pytest.mark.autograd @@ -3670,7 +3649,7 @@ def test_jax(self, tol, broadcast): res = self.cost_fn(weights, coeffs1, coeffs2, dev, broadcast) expected = self.cost_fn_expected(weights, coeffs1, coeffs2) - assert np.allclose(res, np.array(expected), atol=tol, rtol=0) + assert np.allclose(np.array(res)[:, :2], np.array(expected), atol=tol, rtol=0) # TODO: test when Hessians are supported with the new return types # second derivative wrt to Hamiltonian coefficients should be zero 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 1967a44b20a..34478ccecd0 100644 --- a/tests/gradients/parameter_shift/test_parameter_shift_shot_vec.py +++ b/tests/gradients/parameter_shift/test_parameter_shift_shot_vec.py @@ -2032,7 +2032,7 @@ def test_not_expval_error(self, broadcast): tape = qml.tape.QuantumScript.from_queue(q, shots=shot_vec) tape.trainable_params = {2, 3, 4} - with pytest.raises(ValueError, match="for expectations, not var"): + with pytest.raises(ValueError, match="for expectations, not"): qml.gradients.param_shift(tape, broadcast=broadcast) def test_no_trainable_coeffs(self, mocker, broadcast, tol): @@ -2078,7 +2078,6 @@ def test_no_trainable_coeffs(self, mocker, broadcast, tol): b * np.cos(x) * np.cos(y) - c * np.cos(y) * np.sin(x), ] for res in all_res: - assert isinstance(res, tuple) assert len(res) == 2 assert res[0].shape == () assert res[1].shape == () @@ -2086,17 +2085,16 @@ def test_no_trainable_coeffs(self, mocker, broadcast, tol): assert np.allclose(res[0], expected[0], atol=tol, rtol=0) assert np.allclose(res[1], expected[1], atol=tol, rtol=0) - def test_trainable_coeffs(self, mocker, broadcast, tol): + def test_trainable_coeffs(self, broadcast, tol): """Test trainable Hamiltonian coefficients""" shot_vec = many_shots_shot_vector dev = qml.device("default.qubit", wires=2, shots=shot_vec) - spy = mocker.spy(qml.gradients, "hamiltonian_grad") obs = [qml.PauliZ(0), qml.PauliZ(0) @ qml.PauliX(1), qml.PauliY(0)] - coeffs = np.array([0.1, 0.2, 0.3]) + coeffs = qml.numpy.array([0.1, 0.2, 0.3]) H = qml.Hamiltonian(coeffs, obs) - weights = np.array([0.4, 0.5]) + weights = qml.numpy.array([0.4, 0.5]) with qml.queuing.AnnotatedQueue() as q: qml.RX(weights[0], wires=0) @@ -2116,19 +2114,16 @@ def test_trainable_coeffs(self, mocker, broadcast, tol): tapes, fn = qml.gradients.param_shift(tape, broadcast=broadcast) # two (broadcasted if broadcast=True) shifts per rotation gate # one circuit per trainable H term - assert len(tapes) == (2 + 2 if broadcast else 2 * 2 + 2) - assert [t.batch_size for t in tapes] == ([2, 2, None, None] if broadcast else [None] * 6) - spy.assert_called() + assert len(tapes) == (2 if broadcast else 2 * 2) + assert [t.batch_size for t in tapes] == ([2, 2] if broadcast else [None] * 4) res = fn(dev.execute(tapes)) assert isinstance(res, tuple) - assert qml.math.shape(res) == (3, 4) + assert qml.math.shape(res) == (3, 2) expected = [ -c * np.cos(x) * np.sin(y) - np.sin(x) * (a + b * np.sin(y)), b * np.cos(x) * np.cos(y) - c * np.cos(y) * np.sin(x), - np.cos(x), - -(np.sin(x) * np.sin(y)), ] for r in res: assert qml.math.allclose(r, expected, atol=shot_vec_tol) diff --git a/tests/tape/test_qscript.py b/tests/tape/test_qscript.py index 7bad7342677..07ba6ae19ab 100644 --- a/tests/tape/test_qscript.py +++ b/tests/tape/test_qscript.py @@ -318,63 +318,21 @@ def test_error_inconsistent_batch_sizes(self, x, rot, y): ): _ = tape.batch_size - @pytest.mark.parametrize( - "m, output_dim", - [ - ([qml.expval(qml.PauliX(0))], 1), - ([qml.expval(qml.PauliX(0)), qml.var(qml.PauliY(1))], 2), - ([qml.probs(wires=(0, 1))], 4), - ([qml.state()], 0), - ([qml.probs((0, 1)), qml.expval(qml.PauliX(0))], 5), - ], - ) - @pytest.mark.parametrize("ops, factor", [([], 1), ([qml.RX([1.2, 2.3, 3.4], wires=0)], 3)]) - def test_update_output_dim(self, m, output_dim, ops, factor): - """Test setting the output_dim property.""" - qs = QuantumScript(ops, m) - - with pytest.warns( - qml.PennyLaneDeprecationWarning, match="The 'output_dim' property is deprecated" - ): - assert qs.output_dim == output_dim * factor - - def test_lazy_batch_size_and_output_dim(self): - """Test that batch_size and output_dim are computed lazily.""" + def test_lazy_batch_size(self): + """Test that batch_size is computed lazily.""" qs = QuantumScript([qml.RX([1.1, 2.2], 0)], [qml.expval(qml.PauliZ(0))]) copied = qs.copy() assert qs._batch_size is _UNSET_BATCH_SIZE - assert qs._output_dim is None # copying did not evaluate them either assert copied._batch_size is _UNSET_BATCH_SIZE - assert copied._output_dim is None # now evaluate it assert qs.batch_size == 2 - assert qs._output_dim is None # setting batch_size didn't set output_dim - with pytest.warns( - qml.PennyLaneDeprecationWarning, match="The 'output_dim' property is deprecated" - ): - assert qs.output_dim == 2 copied = qs.copy() assert qs._batch_size == 2 - assert qs._output_dim == 2 # copied tape has it pre-evaluated assert copied._batch_size == 2 - assert copied._output_dim == 2 - - def test_lazy_setting_output_dim_sets_batch_size(self): - """Test that setting the output_dim also sets the batch_size.""" - qs = QuantumScript([qml.RX([1.1, 2.2], 0)], [qml.expval(qml.PauliZ(0))]) - assert qs._batch_size is _UNSET_BATCH_SIZE - assert qs._output_dim is None - - with pytest.warns( - qml.PennyLaneDeprecationWarning, match="The 'output_dim' property is deprecated" - ): - assert qs.output_dim == 2 # getting this sets both _output_dim and _batch_size - assert qs._output_dim == 2 - assert qs._batch_size == 2 class TestIteration: @@ -583,12 +541,6 @@ def test_shallow_copy(self): assert qs.data == copied_qs.data assert qs.shots is copied_qs.shots - # check that the output dim is identical - with pytest.warns( - qml.PennyLaneDeprecationWarning, match="The 'output_dim' property is deprecated" - ): - assert qs.output_dim == copied_qs.output_dim - # pylint: disable=unnecessary-lambda @pytest.mark.parametrize( "copy_fn", [lambda tape: tape.copy(copy_operations=True), lambda tape: copy.copy(tape)] @@ -621,12 +573,6 @@ def test_shallow_copy_with_operations(self, copy_fn): assert qs.data == copied_qs.data assert qs.shots is copied_qs.shots - # check that the output dim is identical - with pytest.warns( - qml.PennyLaneDeprecationWarning, match="The 'output_dim' property is deprecated" - ): - assert qs.output_dim == copied_qs.output_dim - def test_deep_copy(self): """Test that deep copying a tape works, and copies all constituent data except parameters""" prep = [qml.BasisState(np.array([1, 0]), wires=(0, 1))] @@ -644,12 +590,6 @@ def test_deep_copy(self): assert all(m1 is not m2 for m1, m2 in zip(copied_qs.measurements, qs.measurements)) assert copied_qs.shots is qs.shots - # check that the output dim is identical - with pytest.warns( - qml.PennyLaneDeprecationWarning, match="The 'output_dim' property is deprecated" - ): - assert qs.output_dim == copied_qs.output_dim - # The underlying operation data has also been copied assert copied_qs.operations[0].wires is not qs.operations[0].wires @@ -763,10 +703,6 @@ def test_cached_properties_when_updating_operations(self): tape = QuantumScript(ops, measurements=[qml.counts()], shots=2500, trainable_params=[1]) assert tape.batch_size == 2 - with pytest.warns( - qml.PennyLaneDeprecationWarning, match="The 'output_dim' property is deprecated" - ): - assert tape.output_dim == 2 assert tape.trainable_params == [1] new_ops = [qml.RX([1.2, 2.3, 3.4], 0)] @@ -776,10 +712,6 @@ def test_cached_properties_when_updating_operations(self): assert new_tape.operations == new_ops assert new_tape.batch_size == 3 - with pytest.warns( - qml.PennyLaneDeprecationWarning, match="The 'output_dim' property is deprecated" - ): - assert new_tape.output_dim == 3 assert new_tape.trainable_params == [0] def test_cached_properties_when_updating_measurements(self): @@ -797,10 +729,6 @@ def test_cached_properties_when_updating_measurements(self): assert tape.obs_sharing_wires == [] assert tape.obs_sharing_wires_id == [] - with pytest.warns( - qml.PennyLaneDeprecationWarning, match="The 'output_dim' property is deprecated" - ): - assert tape.output_dim == 2 assert tape.trainable_params == [1] new_measurements = [qml.expval(qml.X(0)), qml.var(qml.Y(0))] @@ -809,10 +737,6 @@ def test_cached_properties_when_updating_measurements(self): assert tape.measurements == measurements assert new_tape.measurements == new_measurements - with pytest.warns( - qml.PennyLaneDeprecationWarning, match="The 'output_dim' property is deprecated" - ): - assert new_tape.output_dim == 4 assert new_tape.obs_sharing_wires == [qml.X(0), qml.Y(0)] assert new_tape.obs_sharing_wires_id == [0, 1] assert new_tape.trainable_params == [0, 1] @@ -1734,14 +1658,3 @@ def test_jax_pytree_integration(qscript_type): assert data[3] == 3.4 assert data[4] == 2.0 assert qml.math.allclose(data[5], eye_mat) - - -@pytest.mark.parametrize("qscript_type", (QuantumScript, qml.tape.QuantumTape)) -@pytest.mark.parametrize("shots", [None, 1, 10]) -def test_output_dim_is_deprecated(qscript_type, shots): - """Test that the output_dim property is deprecated.""" - with pytest.warns( - qml.PennyLaneDeprecationWarning, match="The 'output_dim' property is deprecated" - ): - qscript = qscript_type([], [], shots=shots) - _ = qscript.output_dim diff --git a/tests/templates/test_subroutines/test_qsvt.py b/tests/templates/test_subroutines/test_qsvt.py index 3e16541c2b1..fe0efc8aec5 100644 --- a/tests/templates/test_subroutines/test_qsvt.py +++ b/tests/templates/test_subroutines/test_qsvt.py @@ -399,227 +399,6 @@ def test_copy(self): assert all(p1 is not p2 for p1, p2 in zip(orig_projectors, copy_projectors)) -class Testqsvt_legacy: - """Test the qml.qsvt_legacy function.""" - - def test_qsvt_legacy_deprecated(self): - """Test that my_feature is deprecated.""" - with pytest.warns(qml.PennyLaneDeprecationWarning, match="`qml.qsvt_legacy` is deprecated"): - _ = qml.qsvt_legacy(0.3, [0.1, 0.2], [0]) - - @pytest.mark.parametrize( - ("A", "phis", "wires", "true_mat"), - [ - ( - [[0.1, 0.2], [0.3, 0.4]], - [0.2, 0.3], - [0, 1], - # mathematical order of gates: - qml.matrix(qml.PCPhase(0.2, dim=2, wires=[0, 1])) - @ qml.matrix(qml.BlockEncode([[0.1, 0.2], [0.3, 0.4]], wires=[0, 1])) - @ qml.matrix(qml.PCPhase(0.3, dim=2, wires=[0, 1])), - ), - ( - [[0.3, 0.1], [0.2, 0.9]], - [0.1, 0.2, 0.3], - [0, 1], - # mathematical order of gates: - qml.matrix(qml.PCPhase(0.1, dim=2, wires=[0, 1])) - @ qml.matrix(qml.adjoint(qml.BlockEncode([[0.3, 0.1], [0.2, 0.9]], wires=[0, 1]))) - @ qml.matrix(qml.PCPhase(0.2, dim=2, wires=[0, 1])) - @ qml.matrix(qml.BlockEncode([[0.3, 0.1], [0.2, 0.9]], wires=[0, 1])) - @ qml.matrix(qml.PCPhase(0.3, dim=2, wires=[0, 1])), - ), - ], - ) - def test_output(self, A, phis, wires, true_mat): - """Test that qml.qsvt_legacy produces the correct output.""" - dev = qml.device("default.qubit", wires=len(wires)) - - @qml.qnode(dev) - def circuit(): - qml.qsvt_legacy(A, phis, wires) - return qml.expval(qml.PauliZ(wires=0)) - - observable_mat = np.kron(qml.matrix(qml.PauliZ(0)), np.eye(2)) - true_expval = (np.conj(true_mat).T @ observable_mat @ true_mat)[0, 0] - - with pytest.warns(qml.PennyLaneDeprecationWarning, match="`qml.qsvt_legacy` is deprecated"): - assert np.isclose(circuit(), true_expval) - assert np.allclose(qml.matrix(circuit)(), true_mat) - - @pytest.mark.parametrize( - ("A", "phis", "wires", "result"), - [ - ( - [[0.1, 0.2], [0.3, 0.4]], - [-1.520692517929803, 0.05010380886509347], - [0, 1], - 0.01, - ), # angles from pyqsp give 0.1*x - ( - 0.3, - [-0.8104500678299933, 1.520692517929803, 0.7603462589648997], - [0], - 0.009, - ), # angles from pyqsp give 0.1*x**2 - ( - -1, - [-1.164, 0.3836, 0.383, 0.406], - [0], - -1, - ), # angles from pyqsp give 0.5 * (5 * x**3 - 3 * x) - ], - ) - def test_output_wx(self, A, phis, wires, result): - """Test that qml.qsvt_legacy produces the correct output.""" - dev = qml.device("default.qubit", wires=len(wires)) - - @qml.qnode(dev) - def circuit(): - qml.qsvt_legacy(A, phis, wires, convention="Wx") - return qml.expval(qml.PauliZ(wires=0)) - - with pytest.warns(qml.PennyLaneDeprecationWarning, match="`qml.qsvt_legacy` is deprecated"): - assert np.isclose(np.real(qml.matrix(circuit)())[0][0], result, rtol=1e-3) - - @pytest.mark.parametrize( - ("A", "phis", "wires", "result"), - [ - ( - [[0.1, 0.2], [0.3, 0.4]], - [-1.520692517929803, 0.05010380886509347], - [0, 1], - 0.01, - ), # angles from pyqsp give 0.1*x - ( - 0.3, - [-0.8104500678299933, 1.520692517929803, 0.7603462589648997], - [0], - 0.009, - ), # angles from pyqsp give 0.1*x**2 - ( - -1, - [-1.164, 0.3836, 0.383, 0.406], - [0], - -1, - ), # angles from pyqsp give 0.5 * (5 * x**3 - 3 * x) - ], - ) - def test_matrix_wx(self, A, phis, wires, result): - """Assert that the matrix method produces the expected result using both call signatures.""" - - with pytest.warns(qml.PennyLaneDeprecationWarning, match="`qml.qsvt_legacy` is deprecated"): - m1 = qml.matrix(qml.qsvt_legacy(A, phis, wires, convention="Wx")) - m2 = qml.matrix(qml.qsvt_legacy, wire_order=wires)(A, phis, wires, convention="Wx") - - assert np.isclose(np.real(m1[0, 0]), result, rtol=1e-3) - assert np.allclose(m1, m2) - - @pytest.mark.torch - @pytest.mark.parametrize( - ("input_matrix", "angles", "wires"), - [([[0.1, 0.2], [0.3, 0.4]], [0.1, 0.2], [0, 1])], - ) - def test_qsvt_torch(self, input_matrix, angles, wires): - """Test that the qsvt_legacy function matrix is correct for torch.""" - import torch - - with pytest.warns(qml.PennyLaneDeprecationWarning, match="`qml.qsvt_legacy` is deprecated"): - default_matrix = qml.matrix(qml.qsvt_legacy(input_matrix, angles, wires)) - - input_matrix = torch.tensor(input_matrix, dtype=float) - angles = torch.tensor(angles, dtype=float) - - op = qml.qsvt_legacy(input_matrix, angles, wires) - - assert np.allclose(qml.matrix(op), default_matrix) - assert qml.math.get_interface(qml.matrix(op)) == "torch" - - @pytest.mark.jax - @pytest.mark.parametrize( - ("input_matrix", "angles", "wires"), - [([[0.1, 0.2], [0.3, 0.4]], [0.1, 0.2], [0, 1])], - ) - def test_qsvt_jax(self, input_matrix, angles, wires): - """Test that the qsvt_legacy function matrix is correct for jax.""" - import jax.numpy as jnp - - with pytest.warns(qml.PennyLaneDeprecationWarning, match="`qml.qsvt_legacy` is deprecated"): - - default_matrix = qml.matrix(qml.qsvt_legacy(input_matrix, angles, wires)) - - input_matrix = jnp.array(input_matrix) - angles = jnp.array(angles) - - op = qml.qsvt_legacy(input_matrix, angles, wires) - - assert np.allclose(qml.matrix(op), default_matrix) - assert qml.math.get_interface(qml.matrix(op)) == "jax" - - @pytest.mark.tf - @pytest.mark.parametrize( - ("input_matrix", "angles", "wires"), - [([[0.1, 0.2], [0.3, 0.4]], [0.1, 0.2], [0, 1])], - ) - def test_qsvt_tensorflow(self, input_matrix, angles, wires): - """Test that the qsvt_legacy function matrix is correct for tensorflow.""" - import tensorflow as tf - - with pytest.warns(qml.PennyLaneDeprecationWarning, match="`qml.qsvt_legacy` is deprecated"): - - default_matrix = qml.matrix(qml.qsvt_legacy(input_matrix, angles, wires)) - - input_matrix = tf.Variable(input_matrix) - angles = tf.Variable(angles) - - op = qml.qsvt_legacy(input_matrix, angles, wires) - - assert np.allclose(qml.matrix(op), default_matrix) - assert qml.math.get_interface(qml.matrix(op)) == "tensorflow" - - def test_qsvt_grad(self): - """Test that qml.grad results are the same as finite difference results""" - - @qml.qnode(qml.device("default.qubit", wires=2)) - def circuit(A, phis): - qml.qsvt_legacy( - A, - phis, - wires=[0, 1], - ) - return qml.expval(qml.PauliZ(wires=0)) - - with pytest.warns(qml.PennyLaneDeprecationWarning, match="`qml.qsvt_legacy` is deprecated"): - - A = np.array([[0.1, 0.2], [0.3, 0.4]], dtype=complex, requires_grad=True) - phis = np.array([0.1, 0.2, 0.3], dtype=complex, requires_grad=True) - y = circuit(A, phis) - - mat_grad_results, phi_grad_results = qml.grad(circuit)(A, phis) - - diff = 1e-8 - - manual_mat_results = [ - (circuit(A + np.array([[diff, 0], [0, 0]]), phis) - y) / diff, - (circuit(A + np.array([[0, diff], [0, 0]]), phis) - y) / diff, - (circuit(A + np.array([[0, 0], [diff, 0]]), phis) - y) / diff, - (circuit(A + np.array([[0, 0], [0, diff]]), phis) - y) / diff, - ] - - for idx, result in enumerate(manual_mat_results): - assert np.isclose(result, np.real(mat_grad_results.flatten()[idx]), atol=1e-6) - - manual_phi_results = [ - (circuit(A, phis + np.array([diff, 0, 0])) - y) / diff, - (circuit(A, phis + np.array([0, diff, 0])) - y) / diff, - (circuit(A, phis + np.array([0, 0, diff])) - y) / diff, - ] - - for idx, result in enumerate(manual_phi_results): - assert np.isclose(result, np.real(phi_grad_results[idx]), atol=1e-6) - - phase_angle_data = ( ( [0, 0, 0], @@ -632,43 +411,9 @@ def circuit(A, phis): ) -@pytest.mark.jax -@pytest.mark.parametrize("initial_angles, expected_angles", phase_angle_data) -def test_private_qsp_to_qsvt_jax(initial_angles, expected_angles): - """Test that the _qsp_to_qsvt function is jax compatible""" - import jax.numpy as jnp - - from pennylane.templates.subroutines.qsvt import _qsp_to_qsvt - - initial_angles = jnp.array(initial_angles) - expected_angles = jnp.array(expected_angles) - - computed_angles = _qsp_to_qsvt(initial_angles) - jnp.allclose(computed_angles, expected_angles) - - -def test_global_phase_not_alway_applied(): - """Test that the global phase is not applied if it is 0""" - - with pytest.warns(qml.PennyLaneDeprecationWarning, match="`qml.qsvt_legacy` is deprecated"): - - decomposition = qml.qsvt_legacy( - [1], [0, 1, 2, 3, 4], wires=[0], convention="Wx" - ).decomposition() - for op in decomposition: - assert not isinstance(op, qml.GlobalPhase) - - class Testqsvt: """Test the qml.qsvt function.""" - def test_qsvt_warning(self): - """Test that qsvt through warning.""" - with pytest.warns( - qml.PennyLaneDeprecationWarning, match="You may be trying to use the old `qsvt`" - ): - qml.qsvt([[0.1, 0.2], [0.2, -0.1]], [0.1, 0, 0.1], [0, 1, 2]) - @pytest.mark.parametrize( ("A", "poly", "block_encoding", "encoding_wires"), [ diff --git a/tests/workflow/interfaces/execute/test_autograd.py b/tests/workflow/interfaces/execute/test_autograd.py index 83a3e161654..7bab72e0c81 100644 --- a/tests/workflow/interfaces/execute/test_autograd.py +++ b/tests/workflow/interfaces/execute/test_autograd.py @@ -774,24 +774,32 @@ def cost_fn(x): @pytest.mark.parametrize("execute_kwargs, shots, device_name", test_matrix) +@pytest.mark.parametrize("constructor", (qml.Hamiltonian, qml.dot, "dunders")) class TestHamiltonianWorkflows: """Test that tapes ending with expectations of Hamiltonians provide correct results and gradients""" + # pylint: disable=too-many-arguments, too-many-positional-arguments @pytest.fixture - def cost_fn(self, execute_kwargs, shots, device_name, seed): + def cost_fn(self, execute_kwargs, shots, device_name, seed, constructor): """Cost function for gradient tests""" device = get_device(device_name, seed=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) - H1 = qml.pauli.pauli_sentence(H1).operation() - obs2 = [qml.PauliZ(0)] - H2 = qml.Hamiltonian(coeffs2, obs2) - H2 = qml.pauli.pauli_sentence(H2).operation() + if constructor == "dunders": + H1 = ( + coeffs1[0] * qml.Z(0) + coeffs1[1] * qml.Z(0) @ qml.X(1) + coeffs1[2] * qml.Y(0) + ) + H2 = coeffs2[0] * qml.Z(0) + else: + + obs1 = [qml.Z(0), qml.Z(0) @ qml.X(1), qml.Y(0)] + H1 = constructor(coeffs1, obs1) + + obs2 = [qml.PauliZ(0)] + H2 = constructor(coeffs2, obs2) with qml.queuing.AnnotatedQueue() as q: qml.RX(weights[0], wires=0) @@ -856,12 +864,16 @@ def test_multiple_hamiltonians_not_trainable(self, cost_fn, shots): else: assert np.allclose(res, expected, atol=atol_for_shots(shots), rtol=0) - def test_multiple_hamiltonians_trainable(self, execute_kwargs, cost_fn, shots): + def test_multiple_hamiltonians_trainable(self, execute_kwargs, cost_fn, shots, constructor): """Test hamiltonian with trainable parameters.""" if execute_kwargs["diff_method"] == "adjoint": pytest.skip("trainable hamiltonians not supported with adjoint") - if execute_kwargs["diff_method"] != "backprop": - pytest.xfail(reason="parameter shift derivatives do not yet support sums.") + if constructor == "dunders": + pytest.xfail("autograd does not like constructing an sprod via dunder.") + if shots.has_partitioned_shots: + pytest.xfail( + "multiple hamiltonians with shot vectors does not seem to be differentiable." + ) coeffs1 = pnp.array([0.1, 0.2, 0.3], requires_grad=True) coeffs2 = pnp.array([0.7], requires_grad=True) @@ -878,8 +890,7 @@ def test_multiple_hamiltonians_trainable(self, execute_kwargs, cost_fn, shots): res = pnp.hstack(qml.jacobian(cost_fn)(weights, coeffs1, coeffs2)) expected = self.cost_fn_jacobian(weights, coeffs1, coeffs2) if shots.has_partitioned_shots: - pytest.xfail( - "multiple hamiltonians with shot vectors does not seem to be differentiable." - ) + assert qml.math.allclose(res[:2, :], expected, atol=atol_for_shots(shots), rtol=0) + assert qml.math.allclose(res[2:, :], expected, atol=atol_for_shots(shots), rtol=0) else: assert qml.math.allclose(res, expected, atol=atol_for_shots(shots), rtol=0) diff --git a/tests/workflow/interfaces/execute/test_jax.py b/tests/workflow/interfaces/execute/test_jax.py index e9d07ccabf7..7a7c9468ca5 100644 --- a/tests/workflow/interfaces/execute/test_jax.py +++ b/tests/workflow/interfaces/execute/test_jax.py @@ -718,24 +718,31 @@ def cost_fn(x): @pytest.mark.parametrize("execute_kwargs, shots, device_name", test_matrix) +@pytest.mark.parametrize("constructor", (qml.Hamiltonian, qml.dot, "dunders")) class TestHamiltonianWorkflows: """Test that tapes ending with expectations of Hamiltonians provide correct results and gradients""" + # pylint: disable=too-many-arguments, too-many-positional-arguments @pytest.fixture - def cost_fn(self, execute_kwargs, shots, device_name, seed): + def cost_fn(self, execute_kwargs, shots, device_name, seed, constructor): """Cost function for gradient tests""" device = get_device(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) - H1 = qml.pauli.pauli_sentence(H1).operation() + if constructor == "dunders": + H1 = ( + coeffs1[0] * qml.Z(0) + coeffs1[1] * qml.Z(0) @ qml.X(1) + coeffs1[2] * qml.Y(0) + ) + H2 = coeffs2[0] * qml.Z(0) + else: - obs2 = [qml.PauliZ(0)] - H2 = qml.Hamiltonian(coeffs2, obs2) - H2 = qml.pauli.pauli_sentence(H2).operation() + obs1 = [qml.Z(0), qml.Z(0) @ qml.X(1), qml.Y(0)] + H1 = constructor(coeffs1, obs1) + + obs2 = [qml.PauliZ(0)] + H2 = constructor(coeffs2, obs2) with qml.queuing.AnnotatedQueue() as q: qml.RX(weights[0], wires=0) @@ -807,8 +814,6 @@ def test_multiple_hamiltonians_trainable(self, execute_kwargs, cost_fn, shots): """Test hamiltonian with trainable parameters.""" if execute_kwargs["diff_method"] == "adjoint": pytest.xfail("trainable hamiltonians not supported with adjoint") - if execute_kwargs["diff_method"] != "backprop": - pytest.xfail(reason="parameter shift derivatives do not yet support sums.") coeffs1 = jnp.array([0.1, 0.2, 0.3]) coeffs2 = jnp.array([0.7]) @@ -825,8 +830,7 @@ def test_multiple_hamiltonians_trainable(self, execute_kwargs, cost_fn, shots): res = jnp.hstack(jax.jacobian(cost_fn, argnums=[0, 1, 2])(weights, coeffs1, coeffs2)) expected = self.cost_fn_jacobian(weights, coeffs1, coeffs2) if shots.has_partitioned_shots: - pytest.xfail( - "multiple hamiltonians with shot vectors does not seem to be differentiable." - ) + assert np.allclose(res[:2, :], expected, atol=atol_for_shots(shots), rtol=0) + assert np.allclose(res[2:, :], expected, atol=atol_for_shots(shots), rtol=0) else: assert np.allclose(res, expected, atol=atol_for_shots(shots), rtol=0) diff --git a/tests/workflow/interfaces/execute/test_tensorflow.py b/tests/workflow/interfaces/execute/test_tensorflow.py index 5fe780f0f5a..a9f93f42c77 100644 --- a/tests/workflow/interfaces/execute/test_tensorflow.py +++ b/tests/workflow/interfaces/execute/test_tensorflow.py @@ -729,24 +729,31 @@ def cost_fn(x): @pytest.mark.parametrize("execute_kwargs, shots, device_name", test_matrix) +@pytest.mark.parametrize("constructor", (qml.Hamiltonian, qml.dot, "dunders")) class TestHamiltonianWorkflows: """Test that tapes ending with expectations of Hamiltonians provide correct results and gradients""" + # pylint: disable=too-many-arguments, too-many-positional-arguments @pytest.fixture - def cost_fn(self, execute_kwargs, shots, device_name, seed): + def cost_fn(self, execute_kwargs, shots, device_name, seed, constructor): """Cost function for gradient tests""" device = qml.device(device_name, seed=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) - H1 = qml.pauli.pauli_sentence(H1).operation() + if constructor == "dunders": + H1 = ( + coeffs1[0] * qml.Z(0) + coeffs1[1] * qml.Z(0) @ qml.X(1) + coeffs1[2] * qml.Y(0) + ) + H2 = coeffs2[0] * qml.Z(0) + else: - obs2 = [qml.PauliZ(0)] - H2 = qml.Hamiltonian(coeffs2, obs2) - H2 = qml.pauli.pauli_sentence(H2).operation() + obs1 = [qml.Z(0), qml.Z(0) @ qml.X(1), qml.Y(0)] + H1 = constructor(coeffs1, obs1) + + obs2 = [qml.PauliZ(0)] + H2 = constructor(coeffs2, obs2) with qml.queuing.AnnotatedQueue() as q: qml.RX(weights[0], wires=0) @@ -811,9 +818,6 @@ 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 execute_kwargs["diff_method"] != "backprop": - pytest.xfail(reason="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) weights = tf.Variable([0.4, 0.5], dtype=tf.float64) diff --git a/tests/workflow/interfaces/execute/test_torch.py b/tests/workflow/interfaces/execute/test_torch.py index 10b07951ac3..8d0890368f6 100644 --- a/tests/workflow/interfaces/execute/test_torch.py +++ b/tests/workflow/interfaces/execute/test_torch.py @@ -761,23 +761,30 @@ def cost_fn(x): @pytest.mark.parametrize("execute_kwargs, shots, device_name", test_matrix) +@pytest.mark.parametrize("constructor", (qml.Hamiltonian, qml.dot, "dunders")) class TestHamiltonianWorkflows: """Test that tapes ending with expectations of Hamiltonians provide correct results and gradients""" + # pylint: disable=too-many-arguments, too-many-positional-arguments @pytest.fixture - def cost_fn(self, execute_kwargs, shots, device_name, seed): + def cost_fn(self, execute_kwargs, shots, device_name, seed, constructor): """Cost function for gradient tests""" device = get_device(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) - H1 = qml.pauli.pauli_sentence(H1).operation() + if constructor == "dunders": + H1 = ( + coeffs1[0] * qml.Z(0) + coeffs1[1] * qml.Z(0) @ qml.X(1) + coeffs1[2] * qml.Y(0) + ) + H2 = coeffs2[0] * qml.Z(0) + else: + + obs1 = [qml.Z(0), qml.Z(0) @ qml.X(1), qml.Y(0)] + H1 = constructor(coeffs1, obs1) - obs2 = [qml.PauliZ(0)] - H2 = qml.Hamiltonian(coeffs2, obs2) - H2 = qml.pauli.pauli_sentence(H2).operation() + obs2 = [qml.PauliZ(0)] + H2 = constructor(coeffs2, obs2) with qml.queuing.AnnotatedQueue() as q: qml.RX(weights[0], wires=0) @@ -854,8 +861,6 @@ 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 execute_kwargs["diff_method"] != "backprop": - pytest.xfail(reason="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) @@ -872,8 +877,7 @@ def test_multiple_hamiltonians_trainable(self, execute_kwargs, cost_fn, shots): res = torch.hstack(torch.autograd.functional.jacobian(cost_fn, (weights, coeffs1, coeffs2))) expected = self.cost_fn_jacobian(weights, coeffs1, coeffs2) if shots.has_partitioned_shots: - pytest.xfail( - "multiple hamiltonians with shot vectors does not seem to be differentiable." - ) + assert torch.allclose(res[:2, :], expected, atol=atol_for_shots(shots), rtol=0) + assert torch.allclose(res[2:, :], expected, atol=atol_for_shots(shots), rtol=0) else: assert torch.allclose(res, expected, atol=atol_for_shots(shots), rtol=0)