Source code for pennylane.estimator.resource_config

# 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.
r"""This module contains the ResourceConfig class, which tracks the configuration for resource estimation"""
from __future__ import annotations

from collections.abc import Callable
from enum import StrEnum
from typing import TYPE_CHECKING

from pennylane.estimator.ops.op_math.controlled_ops import CRX, CRY, CRZ
from pennylane.estimator.ops.qubit.matrix_ops import QubitUnitary
from pennylane.estimator.ops.qubit.parametric_ops_single_qubit import RX, RY, RZ
from pennylane.estimator.templates import (
    AliasSampling,
    MPSPrep,
    PrepTHC,
    QROMStatePreparation,
    QubitizeTHC,
    SelectPauliRot,
    SelectTHC,
)
from pennylane.estimator.templates.trotter import TrotterVibrational, TrotterVibronic

if TYPE_CHECKING:
    from pennylane.estimator.resource_operator import ResourceOperator


class DecompositionType(StrEnum):
    """Specifies the type of decomposition to override."""

    ADJOINT = "adj"
    CONTROLLED = "ctrl"
    POW = "pow"
    BASE = "base"


[docs] class ResourceConfig: """Sets the values of precisions and custom decompositions when estimating resources for a quantum workflow. The precisions and custom decompositions of resource operators can be modified using the :meth:`~.pennylane.estimator.resource_config.ResourceConfig.set_precision` and :meth:`~.pennylane.estimator.resource_config.ResourceConfig.set_decomp` functions of the :code:`ResourceConfig` class. **Example** This example shows how to set a custom precision value for every instance of the :code:`RX` gate. .. code-block:: pycon >>> import pennylane.estimator as qre >>> my_config = qre.ResourceConfig() >>> my_config.set_precision(qre.RX, precision=1e-5) >>> res = qre.estimate( ... qre.RX(), ... gate_set={"RZ", "T", "Hadamard"}, ... config=my_config, ... ) >>> print(res) --- Resources: --- Total wires: 1 algorithmic wires: 1 allocated wires: 0 zero state: 0 any state: 0 Total gates : 28 'T': 28 The :code:`ResourceConfig` can also be used to set custom decompositions. The following example shows how to define a custom decomposition for the ``RX`` gate. .. code-block:: pycon >>> def custom_RX_decomp(precision): # RX = H @ RZ @ H ... h = qre.Hadamard.resource_rep() ... rz = qre.RZ.resource_rep(precision) ... return [qre.GateCount(h, 2), qre.GateCount(rz, 1)] >>> >>> my_config = qre.ResourceConfig() >>> my_config.set_decomp(qre.RX, custom_RX_decomp) >>> res = qre.estimate( ... qre.RX(precision=None), ... gate_set={"RZ", "T", "Hadamard"}, ... config=my_config, ... ) >>> print(res) --- Resources: --- Total wires: 1 algorithmic wires: 1 allocated wires: 0 zero state: 0 any state: 0 Total gates : 3 'RZ': 1, 'Hadamard': 2 """ def __init__(self) -> None: _DEFAULT_PRECISION = 1e-9 _DEFAULT_BIT_PRECISION = 15 _DEFAULT_PHASEGRAD_PRECISION = 1e-6 self.resource_op_precisions = { RX: {"precision": _DEFAULT_PRECISION}, RY: {"precision": _DEFAULT_PRECISION}, RZ: {"precision": _DEFAULT_PRECISION}, CRX: {"precision": _DEFAULT_PRECISION}, CRY: {"precision": _DEFAULT_PRECISION}, CRZ: {"precision": _DEFAULT_PRECISION}, SelectPauliRot: {"precision": _DEFAULT_PRECISION}, QubitUnitary: {"precision": _DEFAULT_PRECISION}, AliasSampling: {"precision": _DEFAULT_PRECISION}, MPSPrep: {"precision": _DEFAULT_PRECISION}, QROMStatePreparation: {"precision": _DEFAULT_PRECISION}, SelectTHC: {"rotation_precision": _DEFAULT_BIT_PRECISION}, PrepTHC: {"coeff_precision": _DEFAULT_BIT_PRECISION}, QubitizeTHC: { "coeff_precision": _DEFAULT_BIT_PRECISION, "rotation_precision": _DEFAULT_BIT_PRECISION, }, TrotterVibronic: { "phase_grad_precision": _DEFAULT_PHASEGRAD_PRECISION, "coeff_precision": 1e-3, }, TrotterVibrational: { "phase_grad_precision": _DEFAULT_PHASEGRAD_PRECISION, "coeff_precision": 1e-3, }, } self._custom_decomps = {} self._adj_custom_decomps = {} self._ctrl_custom_decomps = {} self._pow_custom_decomps = {} @property def custom_decomps(self) -> dict[type[ResourceOperator], Callable]: """Returns the dictionary of custom base decompositions.""" return self._custom_decomps @property def adj_custom_decomps(self) -> dict[type[ResourceOperator], Callable]: """Returns the dictionary of custom adjoint decompositions.""" return self._adj_custom_decomps @property def ctrl_custom_decomps(self) -> dict[type[ResourceOperator], Callable]: """Returns the dictionary of custom controlled decompositions.""" return self._ctrl_custom_decomps @property def pow_custom_decomps(self) -> dict[type[ResourceOperator], Callable]: """Returns the dictionary of custom power decompositions.""" return self._pow_custom_decomps def __str__(self) -> str: decomps = [op.__name__ for op in self.custom_decomps] adj_decomps = [f"Adjoint({op.__name__})" for op in self.adj_custom_decomps] ctrl_decomps = [f"Controlled({op.__name__})" for op in self.ctrl_custom_decomps] pow_decomps = [f"Pow({op.__name__})" for op in self.pow_custom_decomps] all_op_strings = decomps + adj_decomps + ctrl_decomps + pow_decomps op_names = ", ".join(all_op_strings) dict_items_str = ",\n".join( f" {key.__name__}: {value!r}" for key, value in self.resource_op_precisions.items() ) formatted_dict = f"{{\n{dict_items_str}\n}}" return ( f"ResourceConfig(\n" f" precisions = {formatted_dict},\n" f" custom decomps = [{op_names}]\n)" ) def __repr__(self) -> str: return f"ResourceConfig(precisions = {self.resource_op_precisions}, custom_decomps = {self.custom_decomps}, adj_custom_decomps = {self.adj_custom_decomps}, ctrl_custom_decomps = {self.ctrl_custom_decomps}, pow_custom_decomps = {self.pow_custom_decomps})"
[docs] def set_precision(self, op_type: type[ResourceOperator], precision: float) -> None: r"""Sets the precision for a given resource operator. This method updates the precision value for operators that use a single tolerance parameter (e.g., for synthesis error). It will raise an error if you attempt to set the precision for an operator that is not configurable or uses bit-precisions. A negative precision will also raise an error. Args: op_type (type[:class:`~.pennylane.estimator.resource_operator.ResourceOperator`]): the operator class for which to set the precision precision (float): The desired precision tolerance. A smaller value corresponds to a higher precision compilation, which may increase the required gate counts. Must be greater than 0. Raises: ValueError: If ``op_type`` is not a configurable operator or if setting the precision for it is not supported, or if ``precision`` is negative. **Example** .. code-block:: python import pennylane.estimator as qre config = qre.ResourceConfig() # Check the default precision default = config.resource_op_precisions[qre.SelectPauliRot]['precision'] print(f"Default precision for SelectPauliRot: {default}") # Set a new precision config.set_precision(qre.SelectPauliRot, precision=1e-5) new = config.resource_op_precisions[qre.SelectPauliRot]['precision'] print(f"New precision for SelectPauliRot: {new}") .. code-block:: pycon Default precision for SelectPauliRot: 1e-09 New precision for SelectPauliRot: 1e-05 """ if precision < 0: raise ValueError(f"Precision must be a non-negative value, but got {precision}.") if op_type not in self.resource_op_precisions: configurable_ops = sorted( [ op.__name__ for op, params in self.resource_op_precisions.items() if "precision" in params ] ) raise ValueError( f"{op_type.__name__} is not a configurable operator. " f"Configurable operators are: {', '.join(configurable_ops)}" ) if "precision" not in self.resource_op_precisions[op_type]: raise ValueError(f"Setting precision for {op_type.__name__} is not supported.") self.resource_op_precisions[op_type]["precision"] = precision
[docs] def set_single_qubit_rot_precision(self, precision: float) -> None: r"""Sets the synthesis precision for all single-qubit rotation gates. This is a convenience method to update the synthesis precision tolerance for all standard single-qubit rotation gates (and their controlled versions) at once. The synthesis precision dictates the precision for compiling rotation gates into a discrete gate set, which in turn affects the number of gates required. This method updates the ``precision`` value for the following operators: :class:`~.pennylane.estimator.RX`, :class:`~.pennylane.estimator.RY`, :class:`~.pennylane.estimator.RZ`, :class:`~.pennylane.estimator.CRX`, :class:`~.pennylane.estimator.CRY`, :class:`~.pennylane.estimator.CRZ`. Args: precision (float): The desired synthesis precision tolerance. A smaller value corresponds to a higher precision compilation, which may increase the required gate counts. Must be greater than ``0``. Raises: ValueError: If ``precision`` is a negative value. **Example** .. code-block:: python import pennylane.estimator as qre config = qre.ResourceConfig() rot_ops = [qre.RX, qre.RY, qre.RZ, qre.CRX, qre.CRY, qre.CRZ] print([config.resource_op_precisions[op]['precision'] for op in rot_ops]) config.set_single_qubit_rot_precision(1e-5) print([config.resource_op_precisions[op]['precision'] for op in rot_ops]) .. code-block:: pycon [1e-09, 1e-09, 1e-09, 1e-09, 1e-09, 1e-09] [1e-05, 1e-05, 1e-05, 1e-05, 1e-05, 1e-05] """ if precision < 0: raise ValueError(f"Precision must be a non-negative value, but got {precision}.") for op in [RX, RY, RZ, CRX, CRY, CRZ]: self.resource_op_precisions[op]["precision"] = precision
[docs] def set_decomp( self, op_type: type[ResourceOperator], decomp_func: Callable, decomp_type: DecompositionType | None = DecompositionType.BASE, ) -> None: """Sets a custom function to override the default resource decomposition. Args: op_type (type[:class:`~.pennylane.estimator.resource_operator.ResourceOperator`]): the operator class whose decomposition is being overriden. decomp_func (Callable): the new resource decomposition function to be set as default. decomp_type (None | DecompositionType): the decomposition type to override. Options are ``"adj"``, ``"pow"``, ``"ctrl"``, and ``"base"``. Default is ``"base"``. Raises: ValueError: If ``decomp_type`` is not a valid decomposition type. .. note:: The new decomposition function ``decomp_func`` should have the same signature as the one it replaces. Specifically, the signature should match the :code:`resource_keys` of the base resource operator class being overriden. **Example** .. code-block:: python import pennylane.estimator as qre def custom_res_decomp(**kwargs): h = qre.resource_rep(qre.Hadamard) s = qre.resource_rep(qre.S) return [qre.GateCount(h, 1), qre.GateCount(s, 2)] .. code-block:: pycon >>> print(qre.estimate(qre.X(), gate_set={"Hadamard", "Z", "S"})) --- Resources: --- Total wires: 1 algorithmic wires: 1 allocated wires: 0 zero state: 0 any state: 0 Total gates : 4 'S': 2, 'Hadamard': 2 >>> config = qre.ResourceConfig() >>> config.set_decomp(qre.X, custom_res_decomp) >>> print(qre.estimate(qre.X(), gate_set={"Hadamard", "Z", "S"}, config=config)) --- Resources: --- Total wires: 1 algorithmic wires: 1 allocated wires: 0 zero state: 0 any state: 0 Total gates : 3 'S': 2, 'Hadamard': 1 """ if decomp_type is None: decomp_type = DecompositionType("base") else: decomp_type = DecompositionType(decomp_type) if decomp_type == DecompositionType.ADJOINT: self._adj_custom_decomps[op_type] = decomp_func elif decomp_type == DecompositionType.CONTROLLED: self._ctrl_custom_decomps[op_type] = decomp_func elif decomp_type == DecompositionType.POW: self._pow_custom_decomps[op_type] = decomp_func elif decomp_type is None or decomp_type == DecompositionType.BASE: self._custom_decomps[op_type] = decomp_func