Source code for pennylane.estimator.compact_hamiltonian

# Copyright 2025 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 classes used to compactly store the metadata of various Hamiltonians which are relevant for resource estimation.
"""
import copy
from dataclasses import dataclass
from typing import Iterable


def _validate_positive_int(name, value):
    """Helper to validate positive integers."""
    if not isinstance(value, int) or value <= 0:
        raise TypeError(f"{name} must be a positive integer, got {value}")


[docs] @dataclass(frozen=True) class CDFHamiltonian: """For a compressed double-factorized (CDF) Hamiltonian, stores the minimum necessary information pertaining to resource estimation. The form of this Hamiltonian is described in `arXiv:2506.15784 <https://arxiv.org/abs/2506.15784>`_. Args: num_orbitals (int): number of spatial orbitals num_fragments (int): number of fragments in the compressed double-factorized (CDF) representation one_norm (float | None): the one-norm of the Hamiltonian Raises: TypeError: if ``num_orbitals``, or ``num_fragments`` is not a positive integer TypeError: if ``one_norm`` is provided but is not a non-negative float or integer .. seealso:: :class:`~.estimator.templates.trotter.TrotterCDF` """ num_orbitals: int num_fragments: int one_norm: float | None = None def __post_init__(self): """Checks the types of the inputs.""" _validate_positive_int("num_orbitals", self.num_orbitals) _validate_positive_int("num_fragments", self.num_fragments) if self.one_norm is not None and not ( isinstance(self.one_norm, (float, int)) and self.one_norm >= 0 ): raise TypeError( f"one_norm, if provided, must be a positive float or integer. Instead received {self.one_norm}" ) if isinstance(self.one_norm, int): object.__setattr__(self, "one_norm", float(self.one_norm))
[docs] @dataclass(frozen=True) class THCHamiltonian: """For a tensor hypercontracted (THC) Hamiltonian, stores the minimum necessary information pertaining to resource estimation. The form of this Hamiltonian is described in `arXiv:2407.04432 <https://arxiv.org/abs/2407.04432>`_. Args: num_orbitals (int): number of spatial orbitals tensor_rank (int): tensor rank of two-body integrals in the tensor hypercontracted (THC) representation one_norm (float | None): the one-norm of the Hamiltonian Raises: TypeError: if ``num_orbitals``, or ``tensor_rank`` is not a positive integer TypeError: if ``one_norm`` is provided but is not a non-negative float or integer .. seealso:: :class:`~.estimator.templates.trotter.TrotterTHC` """ num_orbitals: int tensor_rank: int one_norm: float | None = None def __post_init__(self): """Checks the types of the inputs.""" _validate_positive_int("num_orbitals", self.num_orbitals) _validate_positive_int("tensor_rank", self.tensor_rank) if self.one_norm is not None and not ( isinstance(self.one_norm, (float, int)) and self.one_norm >= 0 ): raise TypeError( f"one_norm, if provided, must be a positive float or integer." f" Instead received {self.one_norm}" ) if isinstance(self.one_norm, int): object.__setattr__(self, "one_norm", float(self.one_norm))
[docs] @dataclass(frozen=True) class VibrationalHamiltonian: """For a vibrational Hamiltonian, stores the minimum necessary information pertaining to resource estimation. The form of this Hamiltonian is described in `arXiv:2504.10602 <https://arxiv.org/pdf/2504.10602>`_. Args: num_modes (int): number of vibrational modes grid_size (int): number of grid points used to discretize each mode taylor_degree (int): degree of the Taylor expansion used in the vibrational representation one_norm (float | None): the one-norm of the Hamiltonian Raises: TypeError: if ``num_modes``, ``grid_size``, or ``taylor_degree`` is not a positive integer TypeError: if ``one_norm`` is provided but is not a non-negative float or integer .. seealso:: :class:`~.estimator.templates.trotter.TrotterVibrational` """ num_modes: int grid_size: int taylor_degree: int one_norm: float | None = None def __post_init__(self): """Checks the types of the inputs.""" _validate_positive_int("num_modes", self.num_modes) _validate_positive_int("grid_size", self.grid_size) _validate_positive_int("taylor_degree", self.taylor_degree) if self.one_norm is not None and not ( isinstance(self.one_norm, (float, int)) and self.one_norm >= 0 ): raise TypeError( f"one_norm, if provided, must be a positive float or integer." f" Instead received {self.one_norm}" ) if isinstance(self.one_norm, int): object.__setattr__(self, "one_norm", float(self.one_norm))
[docs] @dataclass(frozen=True) class VibronicHamiltonian: """For a vibronic Hamiltonian, stores the minimum necessary information pertaining to resource estimation. The form of this Hamiltonian is described in `arXiv:2411.13669 <https://arxiv.org/abs/2411.13669>`_. Args: num_modes (int): number of vibronic modes num_states (int): number of vibronic states grid_size (int): number of grid points used to discretize each mode taylor_degree (int): degree of the Taylor expansion used in the vibronic representation one_norm (float | None): the one-norm of the Hamiltonian Raises: TypeError: if ``num_modes``, ``num_states``, ``grid_size``, or ``taylor_degree`` is not a positive integer TypeError: if ``one_norm`` is provided but is not a non-negative float or integer .. seealso:: :class:`~.estimator.templates.trotter.TrotterVibronic` """ num_modes: int num_states: int grid_size: int taylor_degree: int one_norm: float | None = None def __post_init__(self): """Checks the types of the inputs.""" _validate_positive_int("num_modes", self.num_modes) _validate_positive_int("num_states", self.num_states) _validate_positive_int("grid_size", self.grid_size) _validate_positive_int("taylor_degree", self.taylor_degree) if self.one_norm is not None and not ( isinstance(self.one_norm, (float, int)) and self.one_norm >= 0 ): raise TypeError( f"one_norm, if provided, must be a positive float or integer." f" Instead received {self.one_norm}" ) if isinstance(self.one_norm, int): object.__setattr__(self, "one_norm", float(self.one_norm))
[docs] class PauliHamiltonian: r"""Stores the minimum necessary information required to estimate resources for a Hamiltonian expressed as a linear combination of tensor products of Pauli operators. Args: num_qubits (int): total number of qubits the Hamiltonian acts on pauli_terms (dict[str, int] | Iterable[dict]): A dictionary representing the Hamiltonian terms where the keys are Pauli strings, e.g ``"XY"``, and the values are integers denoting how frequently a Pauli string appears in the Hamiltonian. When a list of dictionaries is provided, each dictionary is interpreted as a commuting group of terms. See the Usage Details section for more information. one_norm (float | int | None): the one-norm of the Hamiltonian Raises: TypeError: if ``pauli_terms`` is not a dictionary ValueError: if ``one_norm`` is provided but is not a non-negative float or integer ValueError: if ``pauli_terms`` contains invalid keys (not Pauli strings) or values (not integers) .. seealso:: :class:`~.estimator.templates.trotter.TrotterPauli`, :class:`~.estimator.templates.select.SelectPauli` **Example** A ``PauliHamiltonian`` is a compact representation which can be used with compatible templates to obtain resource estimates. Consider for example the Hamiltonian: .. math:: \hat{H} = 0.1 \cdot \Sigma^{30}_{j=1} \hat{X}_{j} \hat{X}_{j+1} - 0.05 \cdot \Sigma^{30}_{k=1} \hat{Y}_{k} \hat{Y}_{k+1} + 0.25 \cdot \Sigma^{40}_{l=1} \hat{X}_{l} This Hamiltonian is represented in a compact form using ``PauliHamiltonian``: >>> import pennylane.estimator as qre >>> pauli_ham = qre.PauliHamiltonian( ... num_qubits = 40, ... pauli_terms = {"X":40, "XX":30, "YY":30}, ... one_norm = 14.5, # (|0.1| * 30) + (|-0.05| * 30) + (|0.25| * 40) ... ) >>> pauli_ham PauliHamiltonian(num_qubits=40, one_norm=14.5, pauli_terms={'X': 40, 'XX': 30, 'YY': 30}) The Hamiltonian can be used as input for other subroutines, like :class:`~.estimator.templates.trotter.TrotterPauli`: >>> num_steps, order = (10, 2) >>> res = qre.estimate(qre.TrotterPauli(pauli_ham, num_steps, order)) >>> print(res) --- Resources: --- Total wires: 40 algorithmic wires: 40 allocated wires: 0 zero state: 0 any state: 0 Total gates : 9.400E+4 'T': 8.800E+4, 'CNOT': 2.400E+3, 'Z': 1.200E+3, 'S': 2.400E+3 .. details:: :title: Usage Details The terms of the Hamiltonian can also be separated into groups such that all operators in the group commute. Users can instantiate the ``PauliHamiltonian`` by specifying these groups of terms directly. >>> import pennylane.estimator as qre >>> commuting_groups = [ ... {"X": 40, "XX": 30}, # first commuting group ... {"YY": 30}, # second commuting group ... ] >>> pauli_ham = qre.PauliHamiltonian( ... num_qubits = 40, ... pauli_terms = commuting_groups, ... one_norm = 14.5, # (|0.1| * 30) + (|-0.05| * 30) + (|0.25| * 40) ... ) >>> pauli_ham PauliHamiltonian(num_qubits=40, one_norm=14.5, pauli_terms=[{'X': 40, 'XX': 30}, {'YY': 30}]) Note that providing more information will generally lead to more accurate resource estimates. >>> num_steps, order = (10, 2) >>> res = qre.estimate(qre.TrotterPauli(pauli_ham, num_steps, order)) >>> print(res) --- Resources: --- Total wires: 40 algorithmic wires: 40 allocated wires: 0 zero state: 0 any state: 0 Total gates : 5.014E+4 'T': 4.708E+4, 'CNOT': 1.260E+3, 'Z': 600, 'S': 1.200E+3 """ def __init__( self, num_qubits: int, pauli_terms: dict | Iterable[dict], one_norm: int | float | None = None, ): self._num_qubits = num_qubits if one_norm is not None and not (isinstance(one_norm, (float, int)) and one_norm >= 0): raise ValueError( f"one_norm, if provided, must be a positive float or integer. Instead received {one_norm}" ) if isinstance(pauli_terms, dict): _validate_pauli_terms(pauli_terms) else: for group in pauli_terms: _validate_pauli_terms(group) self._one_norm = one_norm self._pauli_terms = pauli_terms def __repr__(self): """The repr dunder method for the PauliHamiltonian class.""" return f"PauliHamiltonian(num_qubits={self.num_qubits}, one_norm={self.one_norm}, pauli_terms={self.pauli_terms})" def __eq__(self, other: "PauliHamiltonian"): """Check if two PauliHamiltonians are identical""" return all( ( self._num_qubits == other._num_qubits, self._pauli_terms == other._pauli_terms, self._one_norm == other._one_norm, ) ) def __hash__(self): """Hash function for the compact Hamiltonian representation""" if isinstance(self._pauli_terms, dict): hashable_param = _sort_and_freeze(self._pauli_terms) else: hashable_param = tuple(_sort_and_freeze(group) for group in self._pauli_terms) hashable_params = ( self._num_qubits, hashable_param, self._one_norm, ) return hash(hashable_params) @property def num_qubits(self): """The number of qubits the Hamiltonian acts on""" return self._num_qubits @property def one_norm(self): """The one-norm of the Hamiltonian""" return self._one_norm @property def pauli_terms(self): """A dictionary representing the distribution of Pauli words in the Hamiltonian""" return copy.deepcopy(self._pauli_terms) @property def num_terms(self) -> int: """The total number of Pauli words in the Hamiltonian""" if isinstance(self._pauli_terms, dict): return sum(self._pauli_terms.values()) # Commuting groups are provided return sum(sum(group.values()) for group in self._pauli_terms)
def _sort_and_freeze(pauli_terms: dict) -> tuple[tuple]: """Map a dictionary into a sorted and hashable tuple""" return tuple((k, pauli_terms[k]) for k in sorted(pauli_terms)) def _validate_pauli_terms(pauli_terms: dict) -> bool: """Validate that the ``pauli_terms`` is formatted as expected""" if not isinstance(pauli_terms, dict): raise TypeError( f"Expected `pauli_terms` to be a dictionary or an iterable of dictionaries. got {pauli_terms}" ) for pauli_word, freq in pauli_terms.items(): if (not isinstance(pauli_word, str)) or ( not all(char in {"X", "Y", "Z"} for char in pauli_word) ): raise ValueError( f"The keys represent Pauli words and should be strings containing either 'X','Y' or 'Z' characters only. Got {pauli_word} : {freq}" ) if not (isinstance(freq, int) and (not isinstance(freq, bool)) and (freq >= 0)): raise ValueError( f"The values represent frequencies and should be positive integers, got {pauli_word} : {freq}" )