|  | 
|  | 1 | +# Copyright 2025 Xanadu Quantum Technologies Inc. | 
|  | 2 | + | 
|  | 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | 4 | +# you may not use this file except in compliance with the License. | 
|  | 5 | +# You may obtain a copy of the License at | 
|  | 6 | + | 
|  | 7 | +#     http://www.apache.org/licenses/LICENSE-2.0 | 
|  | 8 | + | 
|  | 9 | +# Unless required by applicable law or agreed to in writing, software | 
|  | 10 | +# distributed under the License is distributed on an "AS IS" BASIS, | 
|  | 11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | 12 | +# See the License for the specific language governing permissions and | 
|  | 13 | +# limitations under the License. | 
|  | 14 | +r"""This module contains the ResourceConfig class, which tracks the configuration for resource estimation""" | 
|  | 15 | + | 
|  | 16 | +from __future__ import annotations | 
|  | 17 | + | 
|  | 18 | +from collections.abc import Callable | 
|  | 19 | +from enum import StrEnum | 
|  | 20 | +from typing import TYPE_CHECKING | 
|  | 21 | + | 
|  | 22 | +if TYPE_CHECKING: | 
|  | 23 | +    from pennylane.estimator.resource_operator import ResourceOperator | 
|  | 24 | + | 
|  | 25 | + | 
|  | 26 | +class DecompositionType(StrEnum): | 
|  | 27 | +    """Specifies the type of decomposition to override.""" | 
|  | 28 | + | 
|  | 29 | +    ADJOINT = "adj" | 
|  | 30 | +    CONTROLLED = "ctrl" | 
|  | 31 | +    POW = "pow" | 
|  | 32 | +    BASE = "base" | 
|  | 33 | + | 
|  | 34 | + | 
|  | 35 | +class ResourceConfig: | 
|  | 36 | +    """A container to track the configuration for precisions and custom decompositions for the | 
|  | 37 | +    resource estimation pipeline. | 
|  | 38 | +    """ | 
|  | 39 | + | 
|  | 40 | +    def __init__(self) -> None: | 
|  | 41 | +        _DEFAULT_PRECISION = 1e-9 | 
|  | 42 | +        _DEFAULT_BIT_PRECISION = 15 | 
|  | 43 | +        self.resource_op_precisions = {} | 
|  | 44 | +        self._custom_decomps = {} | 
|  | 45 | +        self._adj_custom_decomps = {} | 
|  | 46 | +        self._ctrl_custom_decomps = {} | 
|  | 47 | +        self._pow_custom_decomps = {} | 
|  | 48 | + | 
|  | 49 | +    def __str__(self) -> str: | 
|  | 50 | +        decomps = [op.__name__ for op in self._custom_decomps] | 
|  | 51 | +        adj_decomps = [f"Adjoint({op.__name__})" for op in self._adj_custom_decomps] | 
|  | 52 | +        ctrl_decomps = [f"Controlled({op.__name__})" for op in self._ctrl_custom_decomps] | 
|  | 53 | +        pow_decomps = [f"Pow({op.__name__})" for op in self._pow_custom_decomps] | 
|  | 54 | + | 
|  | 55 | +        all_op_strings = decomps + adj_decomps + ctrl_decomps + pow_decomps | 
|  | 56 | +        op_names = ", ".join(all_op_strings) | 
|  | 57 | + | 
|  | 58 | +        dict_items_str = ",\n".join( | 
|  | 59 | +            f"    {key.__name__}: {value!r}" for key, value in self.resource_op_precisions.items() | 
|  | 60 | +        ) | 
|  | 61 | + | 
|  | 62 | +        formatted_dict = f"{{\n{dict_items_str}\n}}" | 
|  | 63 | + | 
|  | 64 | +        return ( | 
|  | 65 | +            f"ResourceConfig(\n" | 
|  | 66 | +            f"  precisions = {formatted_dict},\n" | 
|  | 67 | +            f"  custom decomps = [{op_names}]\n)" | 
|  | 68 | +        ) | 
|  | 69 | + | 
|  | 70 | +    def __repr__(self) -> str: | 
|  | 71 | +        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})" | 
|  | 72 | + | 
|  | 73 | +    def set_precision(self, op_type: type[ResourceOperator], precision: float) -> None: | 
|  | 74 | +        r"""Sets the precision for a given resource operator. | 
|  | 75 | +
 | 
|  | 76 | +        This method updates the precision value for operators that use a single | 
|  | 77 | +        tolerance parameter (e.g., for synthesis error). It will raise an error | 
|  | 78 | +        if you attempt to set the precision for an operator that is not | 
|  | 79 | +        configurable or uses bit-precisions. A negative precision will also raise an error. | 
|  | 80 | +
 | 
|  | 81 | +        Args: | 
|  | 82 | +            op_type (type[ResourceOperator]): the operator class for which | 
|  | 83 | +                to set the precision | 
|  | 84 | +            precision (float): The desired precision tolerance. A smaller | 
|  | 85 | +                value corresponds to a higher precision compilation, which may | 
|  | 86 | +                increase the required gate counts. Must be greater than 0. | 
|  | 87 | +
 | 
|  | 88 | +        Raises: | 
|  | 89 | +            ValueError: If ``op_type`` is not a configurable operator or if setting | 
|  | 90 | +                the precision for it is not supported, or if ``precision`` is negative. | 
|  | 91 | +
 | 
|  | 92 | +        **Example** | 
|  | 93 | +
 | 
|  | 94 | +        .. code-block:: python | 
|  | 95 | +
 | 
|  | 96 | +            import pennylane.estimator as qre | 
|  | 97 | +
 | 
|  | 98 | +            config = qre.ResourceConfig() | 
|  | 99 | +
 | 
|  | 100 | +            # Check the default precision | 
|  | 101 | +            default = config.resource_op_precisions[qre.SelectPauliRot]['precision'] | 
|  | 102 | +            print(f"Default precision for SelectPauliRot: {default}") | 
|  | 103 | +
 | 
|  | 104 | +            # Set a new precision | 
|  | 105 | +            config.set_precision(qre.SelectPauliRot, precision=1e-5) | 
|  | 106 | +            new = config.resource_op_precisions[qre.SelectPauliRot]['precision'] | 
|  | 107 | +            print(f"New precision for SelectPauliRot: {new}") | 
|  | 108 | +
 | 
|  | 109 | +        .. code-block:: pycon | 
|  | 110 | +
 | 
|  | 111 | +            Default precision for SelectPauliRot: 1e-09 | 
|  | 112 | +            New precision for SelectPauliRot: 1e-05 | 
|  | 113 | +        """ | 
|  | 114 | +        if precision < 0: | 
|  | 115 | +            raise ValueError(f"Precision must be a non-negative value, but got {precision}.") | 
|  | 116 | + | 
|  | 117 | +        if op_type not in self.resource_op_precisions: | 
|  | 118 | +            configurable_ops = sorted( | 
|  | 119 | +                [ | 
|  | 120 | +                    op.__name__ | 
|  | 121 | +                    for op, params in self.resource_op_precisions.items() | 
|  | 122 | +                    if "precision" in params | 
|  | 123 | +                ] | 
|  | 124 | +            ) | 
|  | 125 | +            raise ValueError( | 
|  | 126 | +                f"{op_type.__name__} is not a configurable operator. " | 
|  | 127 | +                f"Configurable operators are: {', '.join(configurable_ops)}" | 
|  | 128 | +            ) | 
|  | 129 | + | 
|  | 130 | +        if "precision" not in self.resource_op_precisions[op_type]: | 
|  | 131 | +            raise ValueError(f"Setting precision for {op_type.__name__} is not supported.") | 
|  | 132 | + | 
|  | 133 | +        self.resource_op_precisions[op_type]["precision"] = precision | 
|  | 134 | + | 
|  | 135 | +    def set_single_qubit_rot_precision(self, precision: float) -> None: | 
|  | 136 | +        r"""Sets the synthesis precision for all single-qubit rotation gates. | 
|  | 137 | +
 | 
|  | 138 | +        This is a convenience method to update the synthesis precision tolerance | 
|  | 139 | +        for all standard single-qubit rotation gates (and their | 
|  | 140 | +        controlled versions) at once. The synthesis precision dictates the precision | 
|  | 141 | +        for compiling rotation gates into a discrete gate set, which in turn | 
|  | 142 | +        affects the number of gates required. | 
|  | 143 | +
 | 
|  | 144 | +        This method updates the ``precision`` value for the following operators: | 
|  | 145 | +        :class:`~.pennylane.estimator.RX`, :class:`~.pennylane.estimator.RY`, | 
|  | 146 | +        :class:`~.pennylane.estimator.RZ`, :class:`~.pennylane.estimator.CRX`, | 
|  | 147 | +        :class:`~.pennylane.estimator.CRY`, :class:`~.pennylane.estimator.CRZ`. | 
|  | 148 | +
 | 
|  | 149 | +        Args: | 
|  | 150 | +            precision (float): The desired synthesis precision tolerance. A smaller | 
|  | 151 | +                value corresponds to a higher precision compilation, which may | 
|  | 152 | +                increase the required gate counts. Must be greater than ``0``. | 
|  | 153 | +
 | 
|  | 154 | +        Raises: | 
|  | 155 | +            ValueError: If ``precision`` is a negative value. | 
|  | 156 | +
 | 
|  | 157 | +        **Example** | 
|  | 158 | +
 | 
|  | 159 | +        .. code-block:: python | 
|  | 160 | +
 | 
|  | 161 | +            import pennylane.estimator as qre | 
|  | 162 | +
 | 
|  | 163 | +            config = qre.ResourceConfig() | 
|  | 164 | +            print(f"Default RX precision: {config.resource_op_precisions[qre.RX]['precision']}") | 
|  | 165 | +            print(f"Default RY precision: {config.resource_op_precisions[qre.RY]['precision']}") | 
|  | 166 | +            print(f"Default RZ precision: {config.resource_op_precisions[qre.RZ]['precision']}") | 
|  | 167 | +
 | 
|  | 168 | +            config.set_single_qubit_rot_precision(1e-5) | 
|  | 169 | +            print(f"Updated RX precision: {config.resource_op_precisions[qre.RX]['precision']}") | 
|  | 170 | +            print(f"Updated RY precision: {config.resource_op_precisions[qre.RY]['precision']}") | 
|  | 171 | +            print(f"Updated RZ precision: {config.resource_op_precisions[qre.RZ]['precision']}") | 
|  | 172 | +
 | 
|  | 173 | +        .. code-block:: pycon | 
|  | 174 | +
 | 
|  | 175 | +            Default RX precision: 1e-09 | 
|  | 176 | +            Default RY precision: 1e-09 | 
|  | 177 | +            Default RZ precision: 1e-09 | 
|  | 178 | +            Updated RX precision: 1e-05 | 
|  | 179 | +            Updated RY precision: 1e-05 | 
|  | 180 | +            Updated RZ precision: 1e-05 | 
|  | 181 | +        """ | 
|  | 182 | +        if precision < 0: | 
|  | 183 | +            raise ValueError(f"Precision must be a non-negative value, but got {precision}.") | 
|  | 184 | + | 
|  | 185 | +    def set_decomp( | 
|  | 186 | +        self, | 
|  | 187 | +        op_type: type[ResourceOperator], | 
|  | 188 | +        decomp_func: Callable, | 
|  | 189 | +        decomp_type: DecompositionType | None = DecompositionType.BASE, | 
|  | 190 | +    ) -> None: | 
|  | 191 | +        """Sets a custom function to override the default resource decomposition. | 
|  | 192 | +
 | 
|  | 193 | +        Args: | 
|  | 194 | +            op_type (type[ResourceOperator]): the operator class whose decomposition is being overriden. | 
|  | 195 | +            decomp_func (Callable): the new resource decomposition function to be set as default. | 
|  | 196 | +            decomp_type (None | DecompositionType): the decomposition type to override. Options are | 
|  | 197 | +                ``"adj"``, ``"pow"``, ``"ctrl"``, | 
|  | 198 | +                and ``"base"``. Default is ``"base"``. | 
|  | 199 | +
 | 
|  | 200 | +        Raises: | 
|  | 201 | +            ValueError: If ``decomp_type`` is not a valid decomposition type. | 
|  | 202 | +
 | 
|  | 203 | +        .. note:: | 
|  | 204 | +
 | 
|  | 205 | +            The new decomposition function ``decomp_func`` should have the same signature as the one it replaces. | 
|  | 206 | +            Specifically, the signature should match the :code:`resource_keys` of the base resource | 
|  | 207 | +            operator class being overriden. | 
|  | 208 | +
 | 
|  | 209 | +        **Example** | 
|  | 210 | +
 | 
|  | 211 | +        .. code-block:: python | 
|  | 212 | +
 | 
|  | 213 | +            import pennylane.estimator as qre | 
|  | 214 | +
 | 
|  | 215 | +            def custom_res_decomp(**kwargs): | 
|  | 216 | +                h = qre.resource_rep(qre.Hadamard) | 
|  | 217 | +                s = qre.resource_rep(qre.S) | 
|  | 218 | +                return [qre.GateCount(h, 1), qre.GateCount(s, 2)] | 
|  | 219 | +
 | 
|  | 220 | +        .. code-block:: pycon | 
|  | 221 | +
 | 
|  | 222 | +            >>> print(qre.estimate_resources(qre.X(), gate_set={"Hadamard", "Z", "S"})) | 
|  | 223 | +            --- Resources: --- | 
|  | 224 | +            Total qubits: 1 | 
|  | 225 | +            Total gates : 4 | 
|  | 226 | +            Qubit breakdown: | 
|  | 227 | +              clean qubits: 0, dirty qubits: 0, algorithmic qubits: 1 | 
|  | 228 | +            Gate breakdown: | 
|  | 229 | +              {'Hadamard': 2, 'S': 2} | 
|  | 230 | +            >>> config = qre.ResourceConfig() | 
|  | 231 | +            >>> config.set_decomp(qre.X, custom_res_decomp) | 
|  | 232 | +            >>> print(qre.estimate_resources(qre.X(), gate_set={"Hadamard", "Z", "S"}, config=config)) | 
|  | 233 | +            --- Resources: --- | 
|  | 234 | +            Total qubits: 1 | 
|  | 235 | +            Total gates : 3 | 
|  | 236 | +            Qubit breakdown: | 
|  | 237 | +              clean qubits: 0, dirty qubits: 0, algorithmic qubits: 1 | 
|  | 238 | +            Gate breakdown: | 
|  | 239 | +              {'S': 1, 'Hadamard': 2} | 
|  | 240 | +        """ | 
|  | 241 | +        if decomp_type is None: | 
|  | 242 | +            decomp_type = DecompositionType("base") | 
|  | 243 | +        else: | 
|  | 244 | +            decomp_type = DecompositionType(decomp_type) | 
|  | 245 | + | 
|  | 246 | +        if decomp_type == DecompositionType.ADJOINT: | 
|  | 247 | +            self._adj_custom_decomps[op_type] = decomp_func | 
|  | 248 | +        elif decomp_type == DecompositionType.CONTROLLED: | 
|  | 249 | +            self._ctrl_custom_decomps[op_type] = decomp_func | 
|  | 250 | +        elif decomp_type == DecompositionType.POW: | 
|  | 251 | +            self._pow_custom_decomps[op_type] = decomp_func | 
|  | 252 | +        elif decomp_type is None or decomp_type == DecompositionType.BASE: | 
|  | 253 | +            self._custom_decomps[op_type] = decomp_func | 
0 commit comments