Source code for pprop.pauli.sentence

"""
This module defines :class:`PauliDict`, a mapping from :class:`~pprop.pauli.op.PauliOp`
to a list of trigonometric coefficient terms.

Coefficient representation
--------------------------
Each coefficient is stored as a :data:`CoeffTerms`, 
a list of :data:`CoeffTerm` tuples of the form
``(coeff, sin_idx, cos_idx)``, encoding the product:

.. math::

    c \\prod_{i \\in \\text{sin\\_idx}} \\sin(\\theta_i)
      \\prod_{j \\in \\text{cos\\_idx}} \\cos(\\theta_j)

"""
from __future__ import annotations

from typing import ItemsView, KeysView, ValuesView

from numpy import integer, intp
from pennylane.ops.op_math import sum as qml_sum

from .op import PauliOp

# A single trigonometric product term:  coeff * ∏ sin(θᵢ) * ∏ cos(θⱼ)
CoeffTerm  = tuple[float, list[int], list[int]]   # (scalar, sin_indices, cos_indices)

# The full coefficient of one PauliOp: a sum of CoeffTerms.
CoeffTerms = list[CoeffTerm]

[docs] class PauliDict: """ A mapping from :class:`~pprop.pauli.op.PauliOp` to :data:`CoeffTerms`. Each :class:`~pprop.pauli.op.PauliOp` key maps to a *list* of :data:`CoeffTerm` tuples, where each tuple encodes one trigonometric product term. The full coefficient at parameters :math:`\\boldsymbol{\\theta}` is: .. math:: \\sum_k c_k \\prod_{i \\in S_k} \\sin(\\theta_i) \\prod_{j \\in C_k} \\cos(\\theta_j) where :math:`(c_k, S_k, C_k)` ranges over the list stored for that key. Parameters ---------- data : dict, optional Initial mapping of ``PauliOp -> CoeffTerms``. If ``None`` (default), an empty dict is used. Examples -------- >>> d = PauliDict() >>> key = PauliOp(0b01, 0b00) # X on qubit 0 >>> d.add_term(key, (0.5, [0], [1])) # 0.5 * sin(θ₀) * cos(θ₁) >>> d.add_term(key, (0.5, [], [0, 1])) # 0.5 * cos(θ₀) * cos(θ₁) """ __slots__ = ("_dict",) def __init__(self, data: dict | None = None) -> None: self._dict: dict[PauliOp, CoeffTerms] = dict(data) if data is not None else {} def __setitem__(self, key: PauliOp, value: CoeffTerms) -> None: """ Set ``key`` to ``value``, replacing any existing entry. Parameters ---------- key : PauliOp value : CoeffTerms """ self._dict[key] = value def __getitem__(self, key: PauliOp) -> CoeffTerms: """ Return the :data:`CoeffTerms` associated with ``key``. Parameters ---------- key : PauliOp Returns ------- CoeffTerms Raises ------ KeyError If ``key`` is not present. """ return self._dict[key] def __contains__(self, key: PauliOp) -> bool: """Return ``True`` if ``key`` is present in the mapping.""" return key in self._dict def __len__(self) -> int: """Return the number of distinct :class:`~pprop.pauli.op.PauliOp` keys.""" return len(self._dict) def __repr__(self) -> str: """ Return a human-readable representation of the mapping. For mappings with fewer than 100 keys, each Pauli word and its coefficient terms are printed explicitly. For larger mappings a compact summary is returned instead to avoid flooding the terminal. Returns ------- str """ if len(self._dict) < 100: parts = [] for k, terms in self._dict.items(): term_strs = [] for coeff, sin_idxs, cos_idxs in terms: s = f"{coeff:.2f}" for sin_idx in sin_idxs: s += f"*sin(θ_{int(sin_idx)})" if isinstance(sin_idx, (integer, intp)) else f"*sin({sin_idx})" for cos_idx in cos_idxs: s += f"*cos(θ_{int(cos_idx)})" if isinstance(cos_idx, (integer, intp)) else f"*cos({cos_idx})" term_strs.append(s) parts.append(f"({' + '.join(term_strs)})*{k}") return " + ".join(parts) # Fall back to a compact summary for large dicts. return f"PauliDict({len(self._dict)} terms)"
[docs] def items(self) -> ItemsView[PauliOp, CoeffTerms]: """Return a view of ``(PauliOp, CoeffTerms)`` pairs.""" return self._dict.items()
[docs] def keys(self) -> KeysView[PauliOp]: """Return a view of all :class:`~pprop.pauli.op.PauliOp` keys.""" return self._dict.keys()
[docs] def values(self) -> ValuesView[CoeffTerms]: """Return a view of all :data:`CoeffTerms` values.""" return self._dict.values()
[docs] def add_term(self, key: PauliOp, term: CoeffTerm) -> None: """ Append a single :data:`CoeffTerm` to the list for ``key``. This is the primary accumulation method during Heisenberg propagation: each evolved term is appended without any simplification. Parameters ---------- key : PauliOp The Pauli word to which the term belongs. term : CoeffTerm A ``(coeff, sin_indices, cos_indices)`` tuple to append. """ if key in self._dict: self._dict[key].append(term) else: self._dict[key] = [term]
[docs] def add_terms(self, key: PauliOp, terms: CoeffTerms) -> None: """ Extend the coefficient list for ``key`` with multiple :data:`CoeffTerm` tuples. Parameters ---------- key : PauliOp The Pauli word to update. terms : CoeffTerms A list of ``(coeff, sin_indices, cos_indices)`` tuples to append. """ if key in self._dict: self._dict[key].extend(terms) else: self._dict[key] = list(terms)
[docs] def add_terms_from_dict(self, other: PauliDict) -> None: """ Merge all entries from ``other`` into ``self``. For keys present in both dicts the term lists are concatenated; for keys only in ``other`` a copy of their list is inserted. Parameters ---------- other : PauliDict The source mapping to merge from. """ for k, terms in other._dict.items(): if k in self._dict: self._dict[k].extend(terms) else: self._dict[k] = list(terms)
[docs] def remove_keys_from_dict(self, other: PauliDict) -> None: """ Remove all keys from ``self`` that also appear in ``other``. Used by :meth:`__isub__` to discard Pauli words that have been replaced by their evolved counterparts during propagation. Parameters ---------- other : PauliDict Keys to remove. """ other_keys = other._dict.keys() self._dict = {k: v for k, v in self._dict.items() if k not in other_keys}
def __iadd__(self, other: PauliDict) -> PauliDict: """ Merge ``other`` into ``self`` in-place (``self += other``). Parameters ---------- other : PauliDict Returns ------- PauliDict ``self``, updated in-place. """ if other._dict: self.add_terms_from_dict(other) return self def __isub__(self, other: PauliDict) -> PauliDict: """ Remove all keys of ``other`` from ``self`` in-place (``self -= other``). Equivalent to calling :meth:`remove_keys_from_dict`. Used during propagation to drop original Pauli words after they have been evolved. Parameters ---------- other : PauliDict Returns ------- PauliDict ``self``, updated in-place. """ self.remove_keys_from_dict(other) return self
[docs] @classmethod def from_qml(cls, qml_op) -> PauliDict: """ Construct a :class:`PauliDict` from a PennyLane operator. The operator is decomposed into a sum of Pauli words via :func:`pennylane.ops.op_math.sum`. Each Pauli word receives a constant (parameter-independent) coefficient, encoded as a :data:`CoeffTerm` with empty ``sin_idx`` and ``cos_idx`` lists. Parameters ---------- qml_op : pennylane.operation.Operator A PennyLane observable, typically the output of ``qml.expval(...)``. Returns ------- PauliDict """ result = cls() for c, w in zip(*qml_sum(qml_op).terms()): # Constant coefficients have no sin/cos dependence, so both index # lists are empty, this is a valid CoeffTerm with frequency 0. result.add_term(PauliOp.from_qml(w), (float(c), [], [])) return result