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)