diff --git a/doc/development/deprecations.rst b/doc/development/deprecations.rst index 5bd83558324..1735f59a270 100644 --- a/doc/development/deprecations.rst +++ b/doc/development/deprecations.rst @@ -31,12 +31,6 @@ Pending deprecations - Deprecated in v0.38 - Will be removed in v0.39 -* The functions ``qml.transforms.sum_expand`` and ``qml.transforms.hamiltonian_expand`` are deprecated. - Instead, ``qml.transforms.split_non_commuting`` can be used for equivalent behaviour. - - - Deprecated in v0.38 - - Will be removed in v0.39 - * The ``expansion_strategy`` attribute of ``qml.QNode`` is deprecated. Users should make use of ``qml.workflow.construct_batch``, should they require fine control over the output tape(s). @@ -129,6 +123,12 @@ Other deprecations Completed deprecation cycles ---------------------------- +* The functions ``qml.transforms.sum_expand`` and ``qml.transforms.hamiltonian_expand`` are deprecated. + Instead, ``qml.transforms.split_non_commuting`` can be used for equivalent behaviour. + + - Deprecated in v0.38 + - Removed in v0.39 + * ``queue_idx`` attribute has been removed from the ``Operator``, ``CompositeOp``, and ``SymboliOp`` classes. Instead, the index is now stored as the label of the ``CircuitGraph.graph`` nodes. - Deprecated in v0.38 diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 2baee1a828b..be08cf91ec4 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -19,6 +19,9 @@

Breaking changes 💔

+* `qml.transforms.hamiltonian_expand` and `qml.transforms.sum_expand` are removed. + Please use `qml.transforms.split_non_commuting` instead. +

Deprecations 👋

Documentation 📝

diff --git a/pennylane/devices/__init__.py b/pennylane/devices/__init__.py index 70441f235e3..91402d637e6 100644 --- a/pennylane/devices/__init__.py +++ b/pennylane/devices/__init__.py @@ -91,10 +91,7 @@ defer_measurements transforms.broadcast_expand - transforms.sum_expand transforms.split_non_commuting - transforms.hamiltonian_expand - Modifiers --------- diff --git a/pennylane/transforms/__init__.py b/pennylane/transforms/__init__.py index 7fde7cc97de..78298d3f7b8 100644 --- a/pennylane/transforms/__init__.py +++ b/pennylane/transforms/__init__.py @@ -114,9 +114,7 @@ ~transforms.split_non_commuting ~transforms.split_to_single_terms ~transforms.broadcast_expand - ~transforms.hamiltonian_expand ~transforms.sign_expand - ~transforms.sum_expand ~transforms.convert_to_numpy_parameters ~apply_controlled_Q ~quantum_monte_carlo @@ -320,7 +318,6 @@ def circuit(params): from .diagonalize_measurements import diagonalize_measurements from .dynamic_one_shot import dynamic_one_shot, is_mcm from .sign_expand import sign_expand -from .hamiltonian_expand import hamiltonian_expand, sum_expand from .split_non_commuting import split_non_commuting from .split_to_single_terms import split_to_single_terms from .insert_ops import insert diff --git a/pennylane/transforms/hamiltonian_expand.py b/pennylane/transforms/hamiltonian_expand.py deleted file mode 100644 index c7d9071f655..00000000000 --- a/pennylane/transforms/hamiltonian_expand.py +++ /dev/null @@ -1,557 +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. -""" -Contains the hamiltonian expand tape transform -""" - -# pylint: disable=protected-access -import warnings -from collections.abc import Sequence -from functools import partial - -import pennylane as qml -from pennylane.measurements import ExpectationMP, MeasurementProcess, Shots -from pennylane.ops import Prod, SProd, Sum -from pennylane.tape import QuantumScript, QuantumScriptBatch -from pennylane.transforms import transform -from pennylane.typing import PostprocessingFn, ResultBatch - - -def grouping_processing_fn(res_groupings, coeff_groupings, batch_size, offset): - """Sums up results for the expectation value of a multi-term observable when grouping is involved. - - Args: - res_groupings (ResultBatch): The results from executing the batch of tapes with grouped observables - coeff_groupings (List[TensorLike]): The coefficients in the same grouped structure as the results - batch_size (Optional[int]): The batch size of the tape and corresponding results - offset (TensorLike): A constant offset from the multi-term observable - - Returns: - Result: The result of the expectation value for a multi-term observable - """ - dot_products = [] - for c_group, r_group in zip(coeff_groupings, res_groupings): - # pylint: disable=no-member - if isinstance(r_group, (tuple, list, qml.numpy.builtins.SequenceBox)): - r_group = qml.math.stack(r_group) - if qml.math.shape(r_group) == (): - r_group = qml.math.reshape(r_group, (1,)) - if batch_size and batch_size > 1 and len(c_group) > 1: - r_group = qml.math.moveaxis(r_group, -1, -2) - - if len(c_group) == 1 and len(r_group) != 1: - dot_products.append(r_group * c_group) - else: - dot_products.append(qml.math.dot(r_group, c_group)) - - summed_dot_products = qml.math.sum(qml.math.stack(dot_products), axis=0) - interface = qml.math.get_deep_interface(res_groupings) - return qml.math.asarray(summed_dot_products + offset, like=interface) - - -def _grouping_hamiltonian_expand(tape): - """Calculate the expectation value of a tape with a multi-term observable using the grouping - present on the observable. - """ - hamiltonian = tape.measurements[0].obs - if hamiltonian.grouping_indices is None: - # explicitly selected grouping, but indices not yet computed - hamiltonian.compute_grouping() - - coeff_groupings = [] - obs_groupings = [] - offset = 0 - coeffs, obs = hamiltonian.terms() - for indices in hamiltonian.grouping_indices: - group_coeffs = [] - obs_groupings.append([]) - for i in indices: - if isinstance(obs[i], qml.Identity): - offset += coeffs[i] - else: - group_coeffs.append(coeffs[i]) - obs_groupings[-1].append(obs[i]) - coeff_groupings.append(qml.math.stack(group_coeffs)) - # make one tape per grouping, measuring the - # observables in that grouping - tapes = [] - for obs in obs_groupings: - new_tape = tape.__class__(tape.operations, (qml.expval(o) for o in obs), shots=tape.shots) - - new_tape = new_tape.expand(stop_at=lambda obj: True) - tapes.append(new_tape) - - return tapes, partial( - grouping_processing_fn, - coeff_groupings=coeff_groupings, - batch_size=tape.batch_size, - offset=offset, - ) - - -def naive_processing_fn(res, coeffs, offset): - """Sum up the results weighted by coefficients to get the expectation value of a multi-term observable. - - Args: - res (ResultBatch): The result of executing a batch of tapes where each tape is a different term in the observable - coeffs (List(TensorLike)): The weights for each result in ``res`` - offset (TensorLike): Any constant offset from the multi-term observable - - Returns: - Result: the expectation value of the multi-term observable - """ - dot_products = [] - for c, r in zip(coeffs, res): - dot_products.append(qml.math.dot(qml.math.squeeze(r), c)) - if len(dot_products) == 0: - return offset - summed_dot_products = qml.math.sum(qml.math.stack(dot_products), axis=0) - return qml.math.convert_like(summed_dot_products + offset, res[0]) - - -def _naive_hamiltonian_expand(tape): - """Calculate the expectation value of a multi-term observable using one tape per term.""" - # make one tape per observable - hamiltonian = tape.measurements[0].obs - tapes = [] - offset = 0 - coeffs = [] - for c, o in zip(*hamiltonian.terms()): - if isinstance(o, qml.Identity): - offset += c - else: - new_tape = tape.__class__(tape.operations, [qml.expval(o)], shots=tape.shots) - tapes.append(new_tape) - coeffs.append(c) - - return tapes, partial(naive_processing_fn, coeffs=coeffs, offset=offset) - - -@transform -def hamiltonian_expand( - tape: QuantumScript, group: bool = True -) -> tuple[QuantumScriptBatch, PostprocessingFn]: - r""" - Splits a tape measuring a Hamiltonian expectation into mutliple tapes of Pauli expectations, - and provides a function to recombine the results. - - Args: - tape (QNode or QuantumTape or Callable): the quantum circuit used when calculating the - expectation value of the Hamiltonian - group (bool): Whether to compute disjoint groups of commuting Pauli observables, leading to fewer tapes. - If grouping information can be found in the Hamiltonian, it will be used even if group=False. - - Returns: - qnode (QNode) or tuple[List[QuantumTape], function]: The transformed circuit as described in :func:`qml.transform `. - - .. warning:: - This function is deprecated and will be removed in version 0.39. - Instead, use :func:`~.transforms.split_non_commuting`. - - **Example** - - Given a Hamiltonian, - - .. code-block:: python3 - - H = qml.Y(2) @ qml.Z(1) + 0.5 * qml.Z(2) + qml.Z(1) - - and a tape of the form, - - .. code-block:: python3 - - ops = [qml.Hadamard(0), qml.CNOT((0,1)), qml.X(2)] - tape = qml.tape.QuantumTape(ops, [qml.expval(H)]) - - We can use the ``hamiltonian_expand`` transform to generate new tapes and a classical - post-processing function for computing the expectation value of the Hamiltonian. - - >>> tapes, fn = qml.transforms.hamiltonian_expand(tape) - - We can evaluate these tapes on a device: - - >>> dev = qml.device("default.qubit", wires=3) - >>> res = dev.execute(tapes) - - Applying the processing function results in the expectation value of the Hamiltonian: - - >>> fn(res) - array(-0.5) - - Fewer tapes can be constructed by grouping commuting observables. This can be achieved - by the ``group`` keyword argument: - - .. code-block:: python3 - - H = qml.Hamiltonian([1., 2., 3.], [qml.Z(0), qml.X(1), qml.X(0)]) - - tape = qml.tape.QuantumTape(ops, [qml.expval(H)]) - - With grouping, the Hamiltonian gets split into two groups of observables (here ``[qml.Z(0)]`` and - ``[qml.X(1), qml.X(0)]``): - - >>> tapes, fn = qml.transforms.hamiltonian_expand(tape) - >>> len(tapes) - 2 - - Without grouping it gets split into three groups (``[qml.Z(0)]``, ``[qml.X(1)]`` and ``[qml.X(0)]``): - - >>> tapes, fn = qml.transforms.hamiltonian_expand(tape, group=False) - >>> len(tapes) - 3 - - Alternatively, if the Hamiltonian has already computed groups, they are used even if ``group=False``: - - .. code-block:: python3 - - obs = [qml.Z(0), qml.X(1), qml.X(0)] - coeffs = [1., 2., 3.] - H = qml.Hamiltonian(coeffs, obs, grouping_type='qwc') - - # the initialisation already computes grouping information and stores it in the Hamiltonian - assert H.grouping_indices is not None - - tape = qml.tape.QuantumTape(ops, [qml.expval(H)]) - - Grouping information has been used to reduce the number of tapes from 3 to 2: - - >>> tapes, fn = qml.transforms.hamiltonian_expand(tape, group=False) - >>> len(tapes) - 2 - """ - - warnings.warn( - "qml.transforms.hamiltonian_expand is deprecated and will be removed in version 0.39. " - "Instead, use qml.transforms.split_non_commuting, which can handle the same measurement type.", - qml.PennyLaneDeprecationWarning, - ) - - if ( - len(tape.measurements) != 1 - or not hasattr(tape.measurements[0].obs, "grouping_indices") - or not isinstance(tape.measurements[0], ExpectationMP) - ): - raise ValueError( - "Passed tape must end in `qml.expval(H)` where H can define grouping_indices" - ) - - hamiltonian = tape.measurements[0].obs - if len(hamiltonian.terms()[1]) == 0: - raise ValueError( - "The Hamiltonian in the tape has no terms defined - cannot perform the Hamiltonian expansion." - ) - - if group or hamiltonian.grouping_indices is not None: - return _grouping_hamiltonian_expand(tape) - return _naive_hamiltonian_expand(tape) - - -def _group_measurements( - measurements: Sequence[MeasurementProcess], indices_and_coeffs: list[list[tuple[int, float]]] -) -> tuple[list[list[MeasurementProcess]], list[list[tuple[int, int, float]]]]: - """Groups measurements that does not have overlapping wires. - - Returns: - measurements (List[List[MeasurementProcess]]): the grouped measurements. Each group - is a list of single-term observable measurements. - indices_and_coeffs (List[List[Tuple[int, float]]]): the indices and coefficients of - the single-term measurements to be combined for each original measurement. This - is a list of lists of tuples. Each list within the list corresponds to an original - measurement, and the tuples within the list refer to the single-term measurements - to be combined for this original measurement. Each tuple is of the form ``(group_idx, - sm_idx, coeff)``, where ``group_idx`` locates the group that this single-term - measurement belongs to, ``sm_idx`` is the index of the measurement within the group, - and ``coeff`` is the coefficient of the measurement. - - """ - - groups = [] # Groups of measurements and the wires each group acts on - new_indices_and_coeffs = [] - # Tracks the measurements that have already been grouped, and their location within the groups - grouped_sm_indices = {} - - for mp_indices_and_coeffs in indices_and_coeffs: - # For each original measurement, add each single-term measurement associated - # with it to an existing group or a new group. - - new_mp_indices_and_coeffs = [] - - for sm_idx, coeff in mp_indices_and_coeffs: - # For each single-term measurement currently associated with this measurement - - if sm_idx in grouped_sm_indices: - # If this single-term measurement has already been grouped, find the group - # that it belongs to and its index within the group, add it to the new list - # of indices and coefficients - new_mp_indices_and_coeffs.append((*grouped_sm_indices[sm_idx], coeff)) - continue - - m = measurements[sm_idx] - - # If this measurement is added to an existing group, the sm_index will be the - # length of the group. If the measurement is added to a new group, the sm_index - # should be 0 as it's the first measurement in the group, and the group index - # will be the current length of the groups. - - if len(m.wires) == 0: # measurement acting on all wires - groups.append((m.wires, [m])) - new_mp_indices_and_coeffs.append((len(groups) - 1, 0, coeff)) - grouped_sm_indices[sm_idx] = (len(groups) - 1, 0) - continue - - op_added = False - for grp_idx, (wires, group) in enumerate(groups): - if len(wires) != 0 and len(qml.wires.Wires.shared_wires([wires, m.wires])) == 0: - group.append(m) - groups[grp_idx] = (wires + m.wires, group) - new_mp_indices_and_coeffs.append((grp_idx, len(group) - 1, coeff)) - grouped_sm_indices[sm_idx] = (grp_idx, len(group) - 1) - op_added = True - break - - if not op_added: - groups.append((m.wires, [m])) - new_mp_indices_and_coeffs.append((len(groups) - 1, 0, coeff)) - grouped_sm_indices[sm_idx] = (len(groups) - 1, 0) - - new_indices_and_coeffs.append(new_mp_indices_and_coeffs) - - return [group[1] for group in groups], new_indices_and_coeffs - - -def _sum_expand_processing_fn_grouping( - res: ResultBatch, - group_sizes: list[int], - shots: Shots, - indices_and_coeffs: list[list[tuple[int, int, float]]], - offsets: list[int], -): - """The processing function for sum_expand with grouping.""" - - res_for_each_mp = [] - for mp_indices_and_coeffs, offset in zip(indices_and_coeffs, offsets): - sub_res = [] - coeffs = [] - for group_idx, sm_idx, coeff in mp_indices_and_coeffs: - r_group = res[group_idx] - group_size = group_sizes[group_idx] - if shots.has_partitioned_shots: - r_group = qml.math.stack(r_group, axis=0) - if group_size > 1: - # Move dimensions around to make things work - r_group = qml.math.moveaxis(r_group, 0, 1) - sub_res.append(r_group[sm_idx] if group_size > 1 else r_group) - coeffs.append(coeff) - res_for_each_mp.append(naive_processing_fn(sub_res, coeffs, offset)) - if shots.has_partitioned_shots: - res_for_each_mp = qml.math.moveaxis(res_for_each_mp, 0, 1) - return res_for_each_mp[0] if len(res_for_each_mp) == 1 else res_for_each_mp - - -def _sum_expand_processing_fn( - res: ResultBatch, - shots: Shots, - indices_and_coeffs: list[list[tuple[int, float]]], - offsets: list[int], -): - """The processing function for sum_expand without grouping.""" - - res_for_each_mp = [] - for mp_indices_and_coeffs, offset in zip(indices_and_coeffs, offsets): - sub_res = [] - coeffs = [] - # For each original measurement, locate the results corresponding to each single-term - # measurement, and construct a subset of results to be processed. - for sm_idx, coeff in mp_indices_and_coeffs: - sub_res.append(res[sm_idx]) - coeffs.append(coeff) - res_for_each_mp.append(naive_processing_fn(sub_res, coeffs, offset)) - if shots.has_partitioned_shots: - res_for_each_mp = qml.math.moveaxis(res_for_each_mp, 0, 1) - return res_for_each_mp[0] if len(res_for_each_mp) == 1 else res_for_each_mp - - -@transform -def sum_expand( - tape: QuantumScript, group: bool = True -) -> tuple[QuantumScriptBatch, PostprocessingFn]: - """Splits a quantum tape measuring a Sum expectation into multiple tapes of summand - expectations, and provides a function to recombine the results. - - Args: - tape (.QuantumTape): the quantum tape used when calculating the expectation value - of the Hamiltonian - group (bool): Whether to compute disjoint groups of Pauli observables acting on different - wires, leading to fewer tapes. - - Returns: - tuple[Sequence[.QuantumTape], Callable]: Returns a tuple containing a list of - quantum tapes to be evaluated, and a function to be applied to these - tape executions to compute the expectation value. - - .. warning:: - This function is deprecated and will be removed in version 0.39. - Instead, use :func:`~.transforms.split_non_commuting`. - - **Example** - - Given a Sum operator, - - .. code-block:: python3 - - S = qml.sum(qml.prod(qml.Y(2), qml.Z(1)), qml.s_prod(0.5, qml.Z(2)), qml.Z(1)) - - and a tape of the form, - - .. code-block:: python3 - - ops = [qml.Hadamard(0), qml.CNOT((0,1)), qml.X(2)] - measurements = [ - qml.expval(S), - qml.expval(qml.Z(0)), - qml.expval(qml.X(1)), - qml.expval(qml.Z(2)) - ] - tape = qml.tape.QuantumTape(ops, measurements) - - We can use the ``sum_expand`` transform to generate new tapes and a classical - post-processing function to speed-up the computation of the expectation value of the `Sum`. - - >>> tapes, fn = qml.transforms.sum_expand(tape, group=False) - >>> for tape in tapes: - ... print(tape.measurements) - [expval(Y(2) @ Z(1))] - [expval(Z(2))] - [expval(Z(1))] - [expval(Z(0))] - [expval(X(1))] - - Five tapes are generated: the first three contain the summands of the `Sum` operator, - and the last two contain the remaining observables. Note that the scalars of the scalar products - have been removed. In the processing function, these values will be multiplied by the result obtained - from executing the tapes. - - Additionally, the observable expval(Z(2)) occurs twice in the original tape, but only once - in the transformed tapes. When there are multipe identical measurements in the circuit, the measurement - is performed once and the outcome is copied when obtaining the final result. This will also be resolved - when the processing function is applied. - - We can evaluate these tapes on a device: - - >>> dev = qml.device("default.qubit", wires=3) - >>> res = dev.execute(tapes) - - Applying the processing function results in the expectation value of the Hamiltonian: - - >>> fn(res) - [-0.5, 0.0, 0.0, -0.9999999999999996] - - Fewer tapes can be constructed by grouping observables acting on different wires. This can be achieved - by the ``group`` keyword argument: - - .. code-block:: python3 - - S = qml.sum(qml.Z(0), qml.s_prod(2, qml.X(1)), qml.s_prod(3, qml.X(0))) - - ops = [qml.Hadamard(0), qml.CNOT((0,1)), qml.X(2)] - tape = qml.tape.QuantumTape(ops, [qml.expval(S)]) - - With grouping, the Sum gets split into two groups of observables (here - ``[qml.Z(0), qml.s_prod(2, qml.X(1))]`` and ``[qml.s_prod(3, qml.X(0))]``): - - >>> tapes, fn = qml.transforms.sum_expand(tape, group=True) - >>> for tape in tapes: - ... print(tape.measurements) - [expval(Z(0)), expval(X(1))] - [expval(X(0))] - - """ - - warnings.warn( - "qml.transforms.sum_expand is deprecated and will be removed in version 0.39. " - "Instead, use qml.transforms.split_non_commuting, which can handle the same measurement type.", - qml.PennyLaneDeprecationWarning, - ) - - # The dictionary of all unique single-term observable measurements, and their indices - # within the list of all single-term observable measurements. - single_term_obs_measurements = {} - - # Indices and coefficients of single-term observable measurements to be combined for each - # original measurement. Each element is a list of tuples of the form (index, coeff) - all_sm_indices_and_coeffs = [] - - # Offsets associated with each original measurement in the tape. - offsets = [] - - sm_idx = 0 # Tracks the number of unique single-term observable measurements - for mp in tape.measurements: - obs = mp.obs - offset = 0 - # Indices and coefficients of each single-term observable measurement to be - # combined for this original measurement. - sm_indices_and_coeffs = [] - if isinstance(mp, ExpectationMP) and isinstance(obs, (Sum, Prod, SProd)): - if isinstance(obs, SProd): - # This is necessary because SProd currently does not flatten into - # multiple terms if the base is a sum, which is needed here. - obs = obs.simplify() - # 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 - if isinstance(o, qml.Identity): - offset += c - # If the single-term measurement already exists, it can be reused by all - # original measurements. In this case, add the existing single-term measurement - # to the list corresponding to this original measurement. - # pylint: disable=superfluous-parens - elif (sm := qml.expval(o)) in single_term_obs_measurements: - sm_indices_and_coeffs.append((single_term_obs_measurements[sm], c)) - # Otherwise, add this new measurement to the list of single-term measurements. - else: - single_term_obs_measurements[sm] = sm_idx - sm_indices_and_coeffs.append((sm_idx, c)) - sm_idx += 1 - else: - # For all other measurement types, simply add them to the list of measurements. - if mp not in single_term_obs_measurements: - single_term_obs_measurements[mp] = sm_idx - sm_indices_and_coeffs.append((sm_idx, 1)) - sm_idx += 1 - else: - sm_indices_and_coeffs.append((single_term_obs_measurements[mp], 1)) - - all_sm_indices_and_coeffs.append(sm_indices_and_coeffs) - offsets.append(offset) - - measurements = list(single_term_obs_measurements.keys()) - if group: - groups, indices_and_coeffs = _group_measurements(measurements, all_sm_indices_and_coeffs) - tapes = [tape.__class__(tape.operations, m_group, shots=tape.shots) for m_group in groups] - group_sizes = [len(m_group) for m_group in groups] - return tapes, partial( - _sum_expand_processing_fn_grouping, - indices_and_coeffs=indices_and_coeffs, - group_sizes=group_sizes, - shots=tape.shots, - offsets=offsets, - ) - - tapes = [tape.__class__(tape.operations, [m], shots=tape.shots) for m in measurements] - return tapes, partial( - _sum_expand_processing_fn, - indices_and_coeffs=all_sm_indices_and_coeffs, - shots=tape.shots, - offsets=offsets, - ) diff --git a/tests/transforms/test_hamiltonian_expand.py b/tests/transforms/test_hamiltonian_expand.py deleted file mode 100644 index 246bfbdeb8c..00000000000 --- a/tests/transforms/test_hamiltonian_expand.py +++ /dev/null @@ -1,926 +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 ``hamiltonian_expand`` transform. -""" -import functools -import warnings - -import numpy as np -import pytest - -import pennylane as qml -from pennylane import numpy as pnp -from pennylane.queuing import AnnotatedQueue -from pennylane.tape import QuantumScript -from pennylane.transforms import hamiltonian_expand, sum_expand - -# Defines the device used for all tests -dev = qml.device("default.qubit", wires=4) - -# Defines circuits to be used in queueing/output tests -with AnnotatedQueue() as q_tape1: - qml.PauliX(0) - H1 = qml.Hamiltonian([1.5], [qml.PauliZ(0) @ qml.PauliZ(1)]) - qml.expval(H1) -tape1 = QuantumScript.from_queue(q_tape1) - -with AnnotatedQueue() as q_tape2: - qml.Hadamard(0) - qml.Hadamard(1) - qml.PauliZ(1) - qml.PauliX(2) - H2 = qml.Hamiltonian( - [1, 3, -2, 1, 1], - [ - qml.PauliX(0) @ qml.PauliZ(2), - qml.PauliZ(2), - qml.PauliX(0), - qml.PauliX(2), - qml.PauliZ(0) @ qml.PauliX(1), - ], - ) - qml.expval(H2) -tape2 = QuantumScript.from_queue(q_tape2) - -H3 = qml.Hamiltonian([1.5, 0.3], [qml.Z(0) @ qml.Z(1), qml.X(1)]) - -with AnnotatedQueue() as q3: - qml.PauliX(0) - qml.expval(H3) - - -tape3 = QuantumScript.from_queue(q3) - -H4 = qml.Hamiltonian( - [1, 3, -2, 1, 1, 1], - [ - qml.PauliX(0) @ qml.PauliZ(2), - qml.PauliZ(2), - qml.PauliX(0), - qml.PauliZ(2), - qml.PauliZ(2), - qml.PauliZ(0) @ qml.PauliX(1) @ qml.PauliY(2), - ], -).simplify() - -with AnnotatedQueue() as q4: - qml.Hadamard(0) - qml.Hadamard(1) - qml.PauliZ(1) - qml.PauliX(2) - - qml.expval(H4) - -tape4 = QuantumScript.from_queue(q4) -TAPES = [tape1, tape2, tape3, tape4] -OUTPUTS = [-1.5, -6, -1.5, -8] - - -class TestHamiltonianExpand: - """Tests for the hamiltonian_expand transform""" - - @pytest.fixture(scope="function", autouse=True) - def capture_warnings(self): - with pytest.warns(qml.PennyLaneDeprecationWarning) as record: - yield - - for w in record: - assert isinstance(w.message, qml.PennyLaneDeprecationWarning) - if "qml.transforms.hamiltonian_expand is deprecated" not in str(w.message): - warnings.warn(w.message, w.category) - else: - assert "qml.transforms.hamiltonian_expand is deprecated" in str(w.message) - - def test_ham_with_no_terms_raises(self): - """Tests that the hamiltonian_expand transform raises an error for a Hamiltonian with no terms.""" - mps = [qml.expval(qml.Hamiltonian([], []))] - qscript = QuantumScript([], mps) - - with pytest.raises( - ValueError, - match="The Hamiltonian in the tape has no terms defined - cannot perform the Hamiltonian expansion.", - ): - qml.transforms.hamiltonian_expand(qscript) - - @pytest.mark.parametrize(("tape", "output"), zip(TAPES, OUTPUTS)) - def test_hamiltonians(self, tape, output): - """Tests that the hamiltonian_expand transform returns the correct value""" - - tapes, fn = hamiltonian_expand(tape) - results = dev.execute(tapes) - expval = fn(results) - - assert np.isclose(output, expval) - - qs = QuantumScript(tape.operations, tape.measurements) - tapes, fn = hamiltonian_expand(qs) - results = dev.execute(tapes) - expval = fn(results) - assert np.isclose(output, expval) - - @pytest.mark.parametrize(("tape", "output"), zip(TAPES, OUTPUTS)) - def test_hamiltonians_no_grouping(self, tape, output): - """Tests that the hamiltonian_expand transform returns the correct value - if we switch grouping off""" - - tapes, fn = hamiltonian_expand(tape, group=False) - results = dev.execute(tapes) - expval = fn(results) - - assert np.isclose(output, expval) - - qs = QuantumScript(tape.operations, tape.measurements) - tapes, fn = hamiltonian_expand(qs, group=False) - results = dev.execute(tapes) - expval = fn(results) - - assert np.isclose(output, expval) - - def test_grouping_is_used(self): - """Test that the grouping in a Hamiltonian is used""" - H = qml.Hamiltonian( - [1.0, 2.0, 3.0], [qml.PauliZ(0), qml.PauliX(1), qml.PauliX(0)], grouping_type="qwc" - ) - assert H.grouping_indices is not None - - with AnnotatedQueue() as q: - qml.Hadamard(wires=0) - qml.CNOT(wires=[0, 1]) - qml.PauliX(wires=2) - qml.expval(H) - - tape = QuantumScript.from_queue(q) - tapes, _ = hamiltonian_expand(tape, group=False) - assert len(tapes) == 2 - - qs = QuantumScript(tape.operations, tape.measurements) - tapes, _ = hamiltonian_expand(qs, group=False) - assert len(tapes) == 2 - - def test_number_of_tapes(self): - """Tests that the the correct number of tapes is produced""" - - H = qml.Hamiltonian([1.0, 2.0, 3.0], [qml.PauliZ(0), qml.PauliX(1), qml.PauliX(0)]) - - with AnnotatedQueue() as q: - qml.Hadamard(wires=0) - qml.CNOT(wires=[0, 1]) - qml.PauliX(wires=2) - qml.expval(H) - - tape = QuantumScript.from_queue(q) - tapes, _ = hamiltonian_expand(tape, group=False) - assert len(tapes) == 3 - - tapes, _ = hamiltonian_expand(tape, group=True) - assert len(tapes) == 2 - - def test_number_of_qscripts(self): - """Tests the correct number of quantum scripts are produced.""" - - H = qml.Hamiltonian([1.0, 2.0, 3.0], [qml.PauliZ(0), qml.PauliX(1), qml.PauliX(0)]) - qs = QuantumScript(measurements=[qml.expval(H)]) - - tapes, _ = hamiltonian_expand(qs, group=False) - assert len(tapes) == 3 - - tapes, _ = hamiltonian_expand(qs, group=True) - assert len(tapes) == 2 - - @pytest.mark.parametrize("shots", [None, 100]) - @pytest.mark.parametrize("group", [True, False]) - def test_shots_attribute(self, shots, group): - """Tests that the shots attribute is copied to the new tapes""" - H = qml.Hamiltonian([1.0, 2.0, 3.0], [qml.PauliZ(0), qml.PauliX(1), qml.PauliX(0)]) - - with AnnotatedQueue() as q: - qml.Hadamard(wires=0) - qml.CNOT(wires=[0, 1]) - qml.PauliX(wires=2) - qml.expval(H) - - tape = QuantumScript.from_queue(q, shots=shots) - new_tapes, _ = hamiltonian_expand(tape, group=group) - - assert all(new_tape.shots == tape.shots for new_tape in new_tapes) - - def test_hamiltonian_error(self): - """Tests that the script passed to hamiltonian_expand must end with a hamiltonian.""" - qscript = QuantumScript(measurements=[qml.expval(qml.PauliZ(0))]) - - with pytest.raises(ValueError, match=r"Passed tape must end in"): - qml.transforms.hamiltonian_expand(qscript) - - @pytest.mark.autograd - def test_hamiltonian_dif_autograd(self, tol): - """Tests that the hamiltonian_expand tape transform is differentiable with the Autograd interface""" - - H = qml.Hamiltonian( - [-0.2, 0.5, 1], [qml.PauliX(1), qml.PauliZ(1) @ qml.PauliY(2), qml.PauliZ(0)] - ) - - var = pnp.array([0.1, 0.67, 0.3, 0.4, -0.5, 0.7, -0.2, 0.5, 1.0], requires_grad=True) - output = 0.42294409781940356 - output2 = [ - 9.68883500e-02, - -2.90832724e-01, - -1.04448033e-01, - -1.94289029e-09, - 3.50307411e-01, - -3.41123470e-01, - 0.0, - -0.43657, - 0.64123, - ] - - with AnnotatedQueue() as q: - for _ in range(2): - qml.RX(np.array(0), wires=0) - qml.RX(np.array(0), wires=1) - qml.RX(np.array(0), wires=2) - qml.CNOT(wires=[0, 1]) - qml.CNOT(wires=[1, 2]) - qml.CNOT(wires=[2, 0]) - - qml.expval(H) - - tape = QuantumScript.from_queue(q) - - def cost(x): - new_tape = tape.bind_new_parameters(x, list(range(9))) - tapes, fn = hamiltonian_expand(new_tape) - res = qml.execute(tapes, dev, qml.gradients.param_shift) - return fn(res) - - assert np.isclose(cost(var), output) - - grad = qml.grad(cost)(var) - assert len(grad) == len(output2) - for g, o in zip(grad, output2): - assert np.allclose(g, o, atol=tol) - - @pytest.mark.tf - def test_hamiltonian_dif_tensorflow(self): - """Tests that the hamiltonian_expand tape transform is differentiable with the Tensorflow interface""" - - import tensorflow as tf - - inner_dev = qml.device("default.qubit") - - H = qml.Hamiltonian( - [-0.2, 0.5, 1], [qml.PauliX(1), qml.PauliZ(1) @ qml.PauliY(2), qml.PauliZ(0)] - ) - var = tf.Variable([[0.1, 0.67, 0.3], [0.4, -0.5, 0.7]], dtype=tf.float64) - output = 0.42294409781940356 - output2 = [ - 9.68883500e-02, - -2.90832724e-01, - -1.04448033e-01, - -1.94289029e-09, - 3.50307411e-01, - -3.41123470e-01, - ] - - with tf.GradientTape() as gtape: - with AnnotatedQueue() as q: - for _i in range(2): - qml.RX(var[_i, 0], wires=0) - qml.RX(var[_i, 1], wires=1) - qml.RX(var[_i, 2], wires=2) - qml.CNOT(wires=[0, 1]) - qml.CNOT(wires=[1, 2]) - qml.CNOT(wires=[2, 0]) - qml.expval(H) - - tape = QuantumScript.from_queue(q) - tapes, fn = hamiltonian_expand(tape) - res = fn(qml.execute(tapes, inner_dev, qml.gradients.param_shift)) - - assert np.isclose(res, output) - - g = gtape.gradient(res, var) - assert np.allclose(list(g[0]) + list(g[1]), output2) - - @pytest.mark.parametrize( - "H, expected", - [ - # Contains only groups with single coefficients - (qml.Hamiltonian([1, 2.0], [qml.PauliZ(0), qml.PauliX(0)]), -1), - # Contains groups with multiple coefficients - (qml.Hamiltonian([1.0, 2.0, 3.0], [qml.X(0), qml.X(0) @ qml.X(1), qml.Z(0)]), -3), - ], - ) - @pytest.mark.parametrize("grouping", [True, False]) - def test_processing_function_shot_vectors(self, H, expected, grouping): - """Tests that the processing function works with shot vectors - and grouping with different number of coefficients in each group""" - - dev_with_shot_vector = qml.device("default.qubit", shots=[(20000, 4)]) - if grouping: - H.compute_grouping() - - @functools.partial(qml.transforms.hamiltonian_expand, group=grouping) - @qml.qnode(dev_with_shot_vector) - def circuit(inputs): - qml.RX(inputs, wires=0) - return qml.expval(H) - - res = circuit(np.pi) - assert qml.math.shape(res) == (4,) - assert qml.math.allclose(res, np.ones((4,)) * expected, atol=0.1) - - @pytest.mark.parametrize( - "H, expected", - [ - # Contains only groups with single coefficients - (qml.Hamiltonian([1, 2.0], [qml.PauliZ(0), qml.PauliX(0)]), [1, 0, -1]), - # Contains groups with multiple coefficients - ( - qml.Hamiltonian([1.0, 2.0, 3.0], [qml.X(0), qml.X(0) @ qml.X(1), qml.Z(0)]), - [3, 0, -3], - ), - ], - ) - @pytest.mark.parametrize("grouping", [True, False]) - def test_processing_function_shot_vectors_broadcasting(self, H, expected, grouping): - """Tests that the processing function works with shot vectors, parameter broadcasting, - and grouping with different number of coefficients in each group""" - - dev_with_shot_vector = qml.device("default.qubit", shots=[(10000, 4)]) - if grouping: - H.compute_grouping() - - @functools.partial(qml.transforms.hamiltonian_expand, group=grouping) - @qml.qnode(dev_with_shot_vector) - def circuit(inputs): - qml.RX(inputs, wires=0) - return qml.expval(H) - - res = circuit([0, np.pi / 2, np.pi]) - assert qml.math.shape(res) == (4, 3) - assert qml.math.allclose(res, qml.math.stack([expected] * 4), atol=0.1) - - def test_constant_offset_grouping(self): - """Test that hamiltonian_expand can handle a multi-term observable with a constant offset and grouping.""" - - H = 2.0 * qml.I() + 3 * qml.X(0) + 4 * qml.X(0) @ qml.Y(1) + qml.Z(0) - tape = qml.tape.QuantumScript([], [qml.expval(H)], shots=50) - batch, fn = qml.transforms.hamiltonian_expand(tape, group=True) - - assert len(batch) == 2 - - tape_0 = qml.tape.QuantumScript( - [qml.RY(-np.pi / 2, 0), qml.RX(np.pi / 2, 1)], - [qml.expval(qml.Z(0)), qml.expval(qml.Z(0) @ qml.Z(1))], - shots=50, - ) - tape_1 = qml.tape.QuantumScript([], [qml.expval(qml.Z(0))], shots=50) - - qml.assert_equal(batch[0], tape_0) - qml.assert_equal(batch[1], tape_1) - - dummy_res = ((1.0, 1.0), 1.0) - processed_res = fn(dummy_res) - assert qml.math.allclose(processed_res, 10.0) - - def test_constant_offset_no_grouping(self): - """Test that hamiltonian_expand can handle a multi-term observable with a constant offset and no grouping..""" - - H = 2.0 * qml.I() + 3 * qml.X(0) + 4 * qml.X(0) @ qml.Y(1) + qml.Z(0) - tape = qml.tape.QuantumScript([], [qml.expval(H)], shots=50) - batch, fn = qml.transforms.hamiltonian_expand(tape, group=False) - - assert len(batch) == 3 - - tape_0 = qml.tape.QuantumScript([], [qml.expval(qml.X(0))], shots=50) - tape_1 = qml.tape.QuantumScript([], [qml.expval(qml.X(0) @ qml.Y(1))], shots=50) - tape_2 = qml.tape.QuantumScript([], [qml.expval(qml.Z(0))], shots=50) - - qml.assert_equal(batch[0], tape_0) - qml.assert_equal(batch[1], tape_1) - qml.assert_equal(batch[2], tape_2) - - dummy_res = (1.0, 1.0, 1.0) - processed_res = fn(dummy_res) - assert qml.math.allclose(processed_res, 10.0) - - def test_only_constant_offset(self): - """Tests that hamiltonian_expand can handle a single Identity observable""" - - H = qml.Hamiltonian([1.5, 2.5], [qml.I(), qml.I()]) - - @functools.partial(qml.transforms.hamiltonian_expand, group=False) - @qml.qnode(dev) - def circuit(): - return qml.expval(H) - - with dev.tracker: - res = circuit() - assert dev.tracker.totals == {} - assert qml.math.allclose(res, 4.0) - - -with AnnotatedQueue() as s_tape1: - qml.PauliX(0) - S1 = qml.s_prod(1.5, qml.sum(qml.prod(qml.PauliZ(0), qml.PauliZ(1)), qml.Identity())) - qml.expval(S1) - qml.state() - qml.expval(S1) - -with AnnotatedQueue() as s_tape2: - qml.Hadamard(0) - qml.Hadamard(1) - qml.PauliZ(1) - qml.PauliX(2) - S2 = qml.sum( - qml.prod(qml.PauliX(0), qml.PauliZ(2)), - qml.s_prod(3, qml.PauliZ(2)), - qml.s_prod(-2, qml.PauliX(0)), - qml.Identity(), - qml.PauliX(2), - qml.prod(qml.PauliZ(0), qml.PauliX(1)), - ) - qml.expval(S2) - qml.probs(op=qml.PauliZ(0)) - qml.expval(S2) - -S3 = qml.sum( - qml.s_prod(1.5, qml.prod(qml.PauliZ(0), qml.PauliZ(1))), - qml.s_prod(0.3, qml.PauliX(1)), - qml.Identity(), -) - -with AnnotatedQueue() as s_tape3: - qml.PauliX(0) - qml.expval(S3) - qml.probs(wires=[1, 3]) - qml.expval(qml.PauliX(1)) - qml.expval(S3) - qml.probs(op=qml.PauliY(0)) - - -S4 = qml.sum( - qml.prod(qml.PauliX(0), qml.PauliZ(2), qml.Identity()), - qml.s_prod(3, qml.PauliZ(2)), - qml.s_prod(-2, qml.PauliX(0)), - qml.s_prod(1.5, qml.Identity()), - qml.PauliZ(2), - qml.PauliZ(2), - qml.prod(qml.PauliZ(0), qml.PauliX(1), qml.PauliY(2)), -) - -with AnnotatedQueue() as s_tape4: - qml.Hadamard(0) - qml.Hadamard(1) - qml.PauliZ(1) - qml.PauliX(2) - qml.expval(S4) - qml.expval(qml.PauliX(2)) - qml.expval(S4) - qml.expval(qml.PauliX(2)) - -s_qscript1 = QuantumScript.from_queue(s_tape1) -s_qscript2 = QuantumScript.from_queue(s_tape2) -s_qscript3 = QuantumScript.from_queue(s_tape3) -s_qscript4 = QuantumScript.from_queue(s_tape4) - -SUM_QSCRIPTS = [s_qscript1, s_qscript2, s_qscript3, s_qscript4] -SUM_OUTPUTS = [ - [ - 0, - np.array( - [ - 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, - 1.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, - ], - [-5, np.array([0.5, 0.5]), -5], - [-0.5, np.array([1.0, 0.0, 0.0, 0.0]), 0.0, -0.5, np.array([0.5, 0.5])], - [-6.5, 0, -6.5, 0], -] - - -class TestSumExpand: - """Tests for the sum_expand transform""" - - @pytest.fixture(scope="function", autouse=True) - def capture_warnings(self): - with pytest.warns(qml.PennyLaneDeprecationWarning) as record: - yield - - for w in record: - assert isinstance(w.message, qml.PennyLaneDeprecationWarning) - if "qml.transforms.sum_expand is deprecated" not in str(w.message): - warnings.warn(w.message, w.category) - else: - assert "qml.transforms.sum_expand is deprecated" in str(w.message) - - def test_observables_on_same_wires(self): - """Test that even if the observables are on the same wires, if they are different operations, they are separated. - This is testing for a case that gave rise to a bug that occured due to a problem in MeasurementProcess.hash. - """ - obs1 = qml.prod(qml.PauliX(0), qml.PauliX(1)) - obs2 = qml.prod(qml.PauliX(0), qml.PauliY(1)) - - circuit = QuantumScript(measurements=[qml.expval(obs1), qml.expval(obs2)]) - batch, _ = sum_expand(circuit) - assert len(batch) == 2 - qml.assert_equal(batch[0][0], qml.expval(obs1)) - qml.assert_equal(batch[1][0], qml.expval(obs2)) - - @pytest.mark.parametrize(("qscript", "output"), zip(SUM_QSCRIPTS, SUM_OUTPUTS)) - def test_sums(self, qscript, output): - """Tests that the sum_expand transform returns the correct value""" - processed, _ = dev.preprocess()[0]([qscript]) - assert len(processed) == 1 - qscript = processed[0] - tapes, fn = sum_expand(qscript) - results = dev.execute(tapes) - expval = fn(results) - - assert all(qml.math.allclose(o, e) for o, e in zip(output, expval)) - - @pytest.mark.parametrize(("qscript", "output"), zip(SUM_QSCRIPTS, SUM_OUTPUTS)) - @pytest.mark.filterwarnings("ignore:Use of 'default.qubit.legacy' is deprecated") - def test_sums_legacy_opmath(self, qscript, output): - """Tests that the sum_expand transform returns the correct value""" - dev_old = qml.device("default.qubit.legacy", wires=4) - tapes, fn = sum_expand(qscript) - results = dev_old.batch_execute(tapes) - expval = fn(results) - - assert all(qml.math.allclose(o, e) for o, e in zip(output, expval)) - - @pytest.mark.parametrize(("qscript", "output"), zip(SUM_QSCRIPTS, SUM_OUTPUTS)) - def test_sums_no_grouping(self, qscript, output): - """Tests that the sum_expand transform returns the correct value - if we switch grouping off""" - processed, _ = dev.preprocess()[0]([qscript]) - assert len(processed) == 1 - qscript = processed[0] - tapes, fn = sum_expand(qscript, group=False) - results = dev.execute(tapes) - expval = fn(results) - - assert all(qml.math.allclose(o, e) for o, e in zip(output, expval)) - - def test_grouping(self): - """Test the grouping functionality""" - S = qml.sum(qml.PauliZ(0), qml.s_prod(2, qml.PauliX(1)), qml.s_prod(3, qml.PauliX(0))) - - with AnnotatedQueue() as q: - qml.Hadamard(wires=0) - qml.CNOT(wires=[0, 1]) - qml.PauliX(wires=2) - qml.expval(S) - - qscript = QuantumScript.from_queue(q) - - tapes, _ = sum_expand(qscript, group=True) - assert len(tapes) == 2 - - def test_number_of_qscripts(self): - """Tests the correct number of quantum scripts are produced.""" - - S = qml.sum(qml.PauliZ(0), qml.s_prod(2, qml.PauliX(1)), qml.s_prod(3, qml.PauliX(0))) - qs = QuantumScript(measurements=[qml.expval(S)]) - - tapes, _ = sum_expand(qs, group=False) - assert len(tapes) == 3 - - tapes, _ = sum_expand(qs, group=True) - assert len(tapes) == 2 - - @pytest.mark.parametrize("shots", [None, 100]) - @pytest.mark.parametrize("group", [True, False]) - def test_shots_attribute(self, shots, group): - """Tests that the shots attribute is copied to the new tapes""" - H = qml.Hamiltonian([1.0, 2.0, 3.0], [qml.PauliZ(0), qml.PauliX(1), qml.PauliX(0)]) - - with AnnotatedQueue() as q: - qml.Hadamard(wires=0) - qml.CNOT(wires=[0, 1]) - qml.PauliX(wires=2) - qml.expval(H) - - tape = QuantumScript.from_queue(q, shots=shots) - new_tapes, _ = sum_expand(tape, group=group) - - assert all(new_tape.shots == tape.shots for new_tape in new_tapes) - - def test_non_sum_tape(self): - """Test that the ``sum_expand`` function returns the input tape if it does not - contain a single measurement with the expectation value of a Sum.""" - - with AnnotatedQueue() as q: - qml.expval(qml.PauliZ(0)) - - tape = QuantumScript.from_queue(q) - - tapes, fn = sum_expand(tape) - - assert len(tapes) == 1 - assert isinstance(list(tapes[0])[0].obs, qml.PauliZ) - # Old return types return a list for a single value: - # e.g. qml.expval(qml.PauliX(0)) = [1.23] - res = [1.23] - assert fn(res) == 1.23 - - @pytest.mark.parametrize("grouping", [True, False]) - def test_prod_tape(self, grouping): - """Tests that ``sum_expand`` works with a single Prod measurement""" - - _dev = qml.device("default.qubit", wires=1) - - @functools.partial(qml.transforms.sum_expand, group=grouping) - @qml.qnode(_dev) - def circuit(): - return qml.expval(qml.prod(qml.PauliZ(0), qml.I())) - - assert circuit() == 1.0 - - @pytest.mark.parametrize("grouping", [True, False]) - def test_sprod_tape(self, grouping): - """Tests that ``sum_expand`` works with a single SProd measurement""" - - _dev = qml.device("default.qubit", wires=1) - - @functools.partial(qml.transforms.sum_expand, group=grouping) - @qml.qnode(_dev) - def circuit(): - return qml.expval(qml.s_prod(1.5, qml.Z(0))) - - assert circuit() == 1.5 - - @pytest.mark.parametrize("grouping", [True, False]) - def test_no_obs_tape(self, grouping): - """Tests tapes with only constant offsets (only measurements on Identity)""" - - _dev = qml.device("default.qubit", wires=1) - - @functools.partial(qml.transforms.sum_expand, group=grouping) - @qml.qnode(_dev) - def circuit(): - return qml.expval(qml.s_prod(1.5, qml.I(0))) - - with _dev.tracker: - res = circuit() - assert _dev.tracker.totals == {} - assert qml.math.allclose(res, 1.5) - - @pytest.mark.parametrize("grouping", [True, False]) - def test_no_obs_tape_multi_measurement(self, grouping): - """Tests tapes with only constant offsets (only measurements on Identity)""" - - _dev = qml.device("default.qubit", wires=1) - - @functools.partial(qml.transforms.sum_expand, group=grouping) - @qml.qnode(_dev) - def circuit(): - return qml.expval(qml.s_prod(1.5, qml.I())), qml.expval(qml.s_prod(2.5, qml.I())) - - with _dev.tracker: - res = circuit() - assert _dev.tracker.totals == {} - assert qml.math.allclose(res, [1.5, 2.5]) - - @pytest.mark.parametrize("grouping", [True, False]) - def test_sum_expand_broadcasting(self, grouping): - """Tests that the sum_expand transform works with broadcasting""" - - _dev = qml.device("default.qubit", wires=3) - - @functools.partial(qml.transforms.sum_expand, group=grouping) - @qml.qnode(_dev) - def circuit(x): - qml.RX(x, wires=0) - qml.RY(x, wires=1) - qml.RX(x, wires=2) - return ( - qml.expval(qml.PauliZ(0)), - qml.expval(qml.prod(qml.PauliZ(1), qml.sum(qml.PauliY(2), qml.PauliX(2)))), - qml.expval(qml.sum(qml.PauliZ(0), qml.s_prod(1.5, qml.PauliX(1)))), - ) - - res = circuit([0, np.pi / 3, np.pi / 2, np.pi]) - - def _expected(theta): - return [ - np.cos(theta / 2) ** 2 - np.sin(theta / 2) ** 2, - -(np.cos(theta / 2) ** 2 - np.sin(theta / 2) ** 2) * np.sin(theta), - np.cos(theta / 2) ** 2 - np.sin(theta / 2) ** 2 + 1.5 * np.sin(theta), - ] - - expected = np.array([_expected(t) for t in [0, np.pi / 3, np.pi / 2, np.pi]]).T - assert qml.math.allclose(res, expected) - - @pytest.mark.parametrize( - "theta", [0, np.pi / 3, np.pi / 2, np.pi, [0, np.pi / 3, np.pi / 2, np.pi]] - ) - @pytest.mark.parametrize("grouping", [True, False]) - def test_sum_expand_shot_vector(self, grouping, theta): - """Tests that the sum_expand transform works with shot vectors""" - - _dev = qml.device("default.qubit", wires=3, shots=[(20000, 5)]) - - @functools.partial(qml.transforms.sum_expand, group=grouping) - @qml.qnode(_dev) - def circuit(x): - qml.RX(x, wires=0) - qml.RY(x, wires=1) - qml.RX(x, wires=2) - return ( - qml.expval(qml.PauliZ(0)), - qml.expval(qml.prod(qml.PauliZ(1), qml.sum(qml.PauliY(2), qml.PauliX(2)))), - qml.expval(qml.sum(qml.PauliZ(0), qml.s_prod(1.5, qml.PauliX(1)))), - ) - - if isinstance(theta, list): - theta = np.array(theta) - - expected = [ - np.cos(theta / 2) ** 2 - np.sin(theta / 2) ** 2, - -(np.cos(theta / 2) ** 2 - np.sin(theta / 2) ** 2) * np.sin(theta), - np.cos(theta / 2) ** 2 - np.sin(theta / 2) ** 2 + 1.5 * np.sin(theta), - ] - - res = circuit(theta) - - if isinstance(theta, np.ndarray): - assert qml.math.shape(res) == (5, 3, 4) - else: - assert qml.math.shape(res) == (5, 3) - - for r in res: - assert qml.math.allclose(r, expected, atol=0.05) - - @pytest.mark.autograd - def test_sum_dif_autograd(self, tol): - """Tests that the sum_expand tape transform is differentiable with the Autograd interface""" - S = qml.sum( - qml.s_prod(-0.2, qml.PauliX(1)), - qml.s_prod(0.5, qml.prod(qml.PauliZ(1), qml.PauliY(2))), - qml.s_prod(1, qml.PauliZ(0)), - ) - - var = pnp.array([0.1, 0.67, 0.3, 0.4, -0.5, 0.7, -0.2, 0.5, 1], requires_grad=True) - output = 0.42294409781940356 - output2 = [ - 9.68883500e-02, - -2.90832724e-01, - -1.04448033e-01, - -1.94289029e-09, - 3.50307411e-01, - -3.41123470e-01, - 0.0, - -4.36578753e-01, - 6.41233474e-01, - ] - - with AnnotatedQueue() as q: - for _ in range(2): - qml.RX(np.array(0), wires=0) - qml.RX(np.array(0), wires=1) - qml.RX(np.array(0), wires=2) - qml.CNOT(wires=[0, 1]) - qml.CNOT(wires=[1, 2]) - qml.CNOT(wires=[2, 0]) - - qml.expval(S) - - qscript = QuantumScript.from_queue(q) - - def cost(x): - new_qscript = qscript.bind_new_parameters(x, list(range(9))) - tapes, fn = sum_expand(new_qscript) - res = qml.execute(tapes, dev, qml.gradients.param_shift) - return fn(res) - - assert np.isclose(cost(var), output) - - grad = qml.grad(cost)(var) - assert len(grad) == len(output2) - for g, o in zip(grad, output2): - assert np.allclose(g, o, atol=tol) - - @pytest.mark.tf - def test_sum_dif_tensorflow(self): - """Tests that the sum_expand tape transform is differentiable with the Tensorflow interface""" - - import tensorflow as tf - - S = qml.sum( - qml.s_prod(-0.2, qml.PauliX(1)), - qml.s_prod(0.5, qml.prod(qml.PauliZ(1), qml.PauliY(2))), - qml.s_prod(1, qml.PauliZ(0)), - ) - var = tf.Variable([[0.1, 0.67, 0.3], [0.4, -0.5, 0.7]], dtype=tf.float64) - output = 0.42294409781940356 - output2 = [ - 9.68883500e-02, - -2.90832724e-01, - -1.04448033e-01, - -1.94289029e-09, - 3.50307411e-01, - -3.41123470e-01, - ] - - with tf.GradientTape() as gtape: - with AnnotatedQueue() as q: - for _i in range(2): - qml.RX(var[_i, 0], wires=0) - qml.RX(var[_i, 1], wires=1) - qml.RX(var[_i, 2], wires=2) - qml.CNOT(wires=[0, 1]) - qml.CNOT(wires=[1, 2]) - qml.CNOT(wires=[2, 0]) - qml.expval(S) - - qscript = QuantumScript.from_queue(q) - tapes, fn = sum_expand(qscript) - res = fn(qml.execute(tapes, dev, qml.gradients.param_shift)) - - assert np.isclose(res, output) - - g = gtape.gradient(res, var) - assert np.allclose(list(g[0]) + list(g[1]), output2) - - @pytest.mark.jax - def test_sum_dif_jax(self, tol): - """Tests that the sum_expand tape transform is differentiable with the Jax interface""" - import jax - from jax import numpy as jnp - - S = qml.sum( - qml.s_prod(-0.2, qml.PauliX(1)), - qml.s_prod(0.5, qml.prod(qml.PauliZ(1), qml.PauliY(2))), - qml.s_prod(1, qml.PauliZ(0)), - ) - - var = jnp.array([0.1, 0.67, 0.3, 0.4, -0.5, 0.7, -0.2, 0.5, 1]) - output = 0.42294409781940356 - output2 = [ - 9.68883500e-02, - -2.90832724e-01, - -1.04448033e-01, - -1.94289029e-09, - 3.50307411e-01, - -3.41123470e-01, - 0.0, - -4.36578753e-01, - 6.41233474e-01, - ] - - with AnnotatedQueue() as q: - for _ in range(2): - qml.RX(np.array(0), wires=0) - qml.RX(np.array(0), wires=1) - qml.RX(np.array(0), wires=2) - qml.CNOT(wires=[0, 1]) - qml.CNOT(wires=[1, 2]) - qml.CNOT(wires=[2, 0]) - - qml.expval(S) - - qscript = QuantumScript.from_queue(q) - - def cost(x): - new_qscript = qscript.bind_new_parameters(x, list(range(9))) - tapes, fn = sum_expand(new_qscript) - res = qml.execute(tapes, dev, qml.gradients.param_shift) - return fn(res) - - assert np.isclose(cost(var), output) - - grad = jax.grad(cost)(var) - assert len(grad) == len(output2) - for g, o in zip(grad, output2): - assert np.allclose(g, o, atol=tol)