Source code for pennylane.transforms.hamiltonian_expand

# 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
from typing import List, Sequence, Callable

import pennylane as qml
from pennylane.measurements import ExpectationMP, MeasurementProcess
from pennylane.ops import SProd, Sum
from pennylane.tape import QuantumScript, QuantumTape
from pennylane.transforms import transform


[docs]@transform def hamiltonian_expand(tape: QuantumTape, group: bool = True) -> (Sequence[QuantumTape], Callable): 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 <pennylane.transform>`. **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 """ if ( len(tape.measurements) != 1 or not isinstance(hamiltonian := tape.measurements[0].obs, qml.Hamiltonian) or not isinstance(tape.measurements[0], ExpectationMP) ): raise ValueError( "Passed tape must end in `qml.expval(H)`, where H is of type `qml.Hamiltonian`" ) if qml.math.shape(hamiltonian.coeffs) == (0,) and qml.math.shape(hamiltonian.ops) == (0,): raise ValueError( "The Hamiltonian in the tape has no terms defined - cannot perform the Hamiltonian expansion." ) # note: for backward passes of some frameworks # it is crucial to use the hamiltonian.data attribute, # and not hamiltonian.coeffs when recombining the results if group or hamiltonian.grouping_indices is not None: if hamiltonian.grouping_indices is None: # explicitly selected grouping, but indices not yet computed hamiltonian.compute_grouping() coeff_groupings = [ qml.math.stack([hamiltonian.data[i] for i in indices]) for indices in hamiltonian.grouping_indices ] obs_groupings = [ [hamiltonian.ops[i] for i in indices] for indices in hamiltonian.grouping_indices ] # 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) def processing_fn(res_groupings): # pylint: disable=no-member res_groupings = [ qml.math.stack(r) if isinstance(r, (tuple, qml.numpy.builtins.SequenceBox)) else r for r in res_groupings ] res_groupings = [ qml.math.reshape(r, (1,)) if r.shape == () else r for r in res_groupings ] dot_products = [] for c_group, r_group in zip(coeff_groupings, res_groupings): if tape.batch_size: r_group = r_group.T 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) return qml.math.convert_like(summed_dot_products, res_groupings[0]) return tapes, processing_fn coeffs = hamiltonian.data # make one tape per observable tapes = [] for o in hamiltonian.ops: # pylint: disable=protected-access new_tape = tape.__class__(tape.operations, [qml.expval(o)], shots=tape.shots) tapes.append(new_tape) # pylint: disable=function-redefined def processing_fn(res): dot_products = [] for c, r in zip(coeffs, res): if qml.math.ndim(c) == 0 and qml.math.size(r) != 1: dot_products.append(qml.math.squeeze(r) * c) else: dot_products.append(qml.math.dot(qml.math.squeeze(r), c)) summed_dot_products = qml.math.sum(qml.math.stack(dot_products), axis=0) return qml.math.convert_like(summed_dot_products, res[0]) return tapes, processing_fn
# pylint: disable=too-many-branches, too-many-statements
[docs]@transform def sum_expand(tape: QuantumTape, group: bool = True) -> (Sequence[QuantumTape], Callable): """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. **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))] """ # Populate these 2 dictionaries with the unique measurement objects, the index of the # initial measurement on the tape and the coefficient # NOTE: expval(Sum) is expanded into the expectation of each summand # NOTE: expval(SProd) is transformed into expval(SProd.base) and the coeff is updated measurements_dict = {} # {m_hash: measurement} idxs_coeffs_dict = {} # {m_hash: [(location_idx, coeff)]} for idx, m in enumerate(tape.measurements): obs = m.obs if isinstance(obs, Sum) and isinstance(m, ExpectationMP): for summand in obs.operands: coeff = 1 if isinstance(summand, SProd): coeff = summand.scalar summand = summand.base s_m = qml.expval(summand) if s_m.hash not in measurements_dict: measurements_dict[s_m.hash] = s_m idxs_coeffs_dict[s_m.hash] = [(idx, coeff)] else: idxs_coeffs_dict[s_m.hash].append((idx, coeff)) continue coeff = 1 if isinstance(m, ExpectationMP) else None if isinstance(obs, SProd) and isinstance(m, ExpectationMP): coeff = obs.scalar m = qml.expval(obs.base) if m.hash not in measurements_dict: measurements_dict[m.hash] = m idxs_coeffs_dict[m.hash] = [(idx, coeff)] else: idxs_coeffs_dict[m.hash].append((idx, coeff)) # Cast the dictionaries into lists (we don't need the hashed anymore) measurements = list(measurements_dict.values()) idxs_coeffs = list(idxs_coeffs_dict.values()) # Create the tapes, group observables if group==True # pylint: disable=too-many-nested-blocks if group: m_groups = _group_measurements(measurements) # Update ``idxs_coeffs`` list such that it tracks the new ``m_groups`` list of lists tmp_idxs = [] for m_group in m_groups: if len(m_group) == 1: # pylint: disable=undefined-loop-variable for i, m in enumerate(measurements): if m is m_group[0]: break tmp_idxs.append(idxs_coeffs[i]) else: inds = [] for mp in m_group: # pylint: disable=undefined-loop-variable for i, m in enumerate(measurements): if m is mp: break inds.append(idxs_coeffs[i]) tmp_idxs.append(inds) idxs_coeffs = tmp_idxs qscripts = [ QuantumScript(ops=tape.operations, measurements=m_group, shots=tape.shots) for m_group in m_groups ] else: qscripts = [ QuantumScript(ops=tape.operations, measurements=[m], shots=tape.shots) for m in measurements ] def processing_fn(expanded_results): results = [] # [(m_idx, result)] for qscript_res, qscript_idxs in zip(expanded_results, idxs_coeffs): if isinstance(qscript_idxs[0], tuple): # qscript_res contains only one result for idx, coeff in qscript_idxs: results.append((idx, qscript_res if coeff is None else coeff * qscript_res)) continue # qscript_res contains multiple results for res, idxs in zip(qscript_res, qscript_idxs): for idx, coeff in idxs: results.append((idx, res if coeff is None else coeff * res)) # sum results by idx res_dict = {} for idx, res in results: if idx in res_dict: res_dict[idx] += res else: res_dict[idx] = res # sort results by idx results = [res_dict[key] for key in sorted(res_dict)] return results[0] if len(results) == 1 else results return qscripts, processing_fn
def _group_measurements(measurements: List[MeasurementProcess]) -> List[List[MeasurementProcess]]: """Group observables of ``measurements`` into groups with non overlapping wires. Args: measurements (List[MeasurementProcess]): list of measurement processes Returns: List[List[MeasurementProcess]]: list of groups of observables with non overlapping wires """ qwc_groups = [] for m in measurements: if len(m.wires) == 0: # measurement acts on all wires: e.g. qml.counts() qwc_groups.append((m.wires, [m])) continue op_added = False for idx, (wires, group) in enumerate(qwc_groups): if len(wires) > 0 and all(wire not in m.wires for wire in wires): qwc_groups[idx] = (wires + m.wires, group + [m]) op_added = True break if not op_added: qwc_groups.append((m.wires, [m])) return [group[1] for group in qwc_groups]