code: 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.PauliY(2), qml.PauliZ(1)), qml.s_prod(0.5, qml.PauliZ(2)), qml.PauliZ(1))
  
      and a tape of the form,
  
      .. code-block:: python3
  
          ops = [qml.Hadamard(0), qml.CNOT((0,1)), qml.PauliX(2)]
          measurements = [
              qml.expval(S),
              qml.expval(qml.PauliZ(0)),
              qml.expval(qml.PauliX(1)),
              qml.expval(qml.PauliZ(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(PauliY(wires=[2]) @ PauliZ(wires=[1]))]
      [expval(PauliZ(wires=[2]))]
      [expval(PauliZ(wires=[1]))]
      [expval(PauliZ(wires=[0]))]
      [expval(PauliX(wires=[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(PauliZ(wires=[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.PauliZ(0), qml.s_prod(2, qml.PauliX(1)), qml.s_prod(3, qml.PauliX(0)))
  
          ops = [qml.Hadamard(0), qml.CNOT((0,1)), qml.PauliX(2)]
          tape = qml.tape.QuantumTape(ops, [qml.expval(S)])
  
      With grouping, the Sum gets split into two groups of observables (here
      ``[qml.PauliZ(0), qml.s_prod(2, qml.PauliX(1))]`` and ``[qml.s_prod(3, qml.PauliX(0))]``):
  
      >>> tapes, fn = qml.transforms.sum_expand(tape, group=True)
      >>> for tape in tapes:
      ...     print(tape.measurements)
      [expval(PauliZ(wires=[0])), expval(PauliX(wires=[1]))]
      [expval(PauliX(wires=[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
  
file_path: pennylane/pennylane/transforms/hamiltonian_expand.py
imports: from typing import List, Sequence, Callable
  import pennylane
  from pennylane.measurements import ExpectationMP, MeasurementProcess
  from pennylane.ops import SProd, Sum
  from pennylane.tape import QuantumScript, QuantumTape
  from pennylane.transforms import transform
label_desc: Use image node_id 2 for calling a global function with example usage: sum_expand(tape, group) and returns: qscripts,  processing_fn, unknown