Source code for pennylane.estimator.templates.select

# 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"""Resource operators for select templates."""

import math
from collections import defaultdict

import numpy as np

import pennylane.estimator as qre
from pennylane.estimator import Allocate, Deallocate
from pennylane.estimator.compact_hamiltonian import PauliHamiltonian, THCHamiltonian
from pennylane.estimator.resource_operator import (
    CompressedResourceOp,
    GateCount,
    ResourceOperator,
    resource_rep,
)
from pennylane.wires import Wires, WiresLike

# pylint: disable=arguments-differ,super-init-not-called, signature-differs


[docs] class SelectTHC(ResourceOperator): r"""Resource class for creating the custom ``Select`` operator for tensor hypercontracted (THC) Hamiltonian. .. note:: This decomposition assumes that an appropriately sized phase gradient state is available. Users should ensure that the cost of constructing this state has been accounted for. See also :class:`~.pennylane.estimator.templates.subroutines.PhaseGradient`. Args: thc_ham (:class:`~pennylane.estimator.compact_hamiltonian.THCHamiltonian`): A tensor hypercontracted Hamiltonian on which this ``Select`` operator is being applied. num_batches (int): The number of batches for loading Givens rotation angles into temporary quantum registers. Must be less than the number of orbitals in ``thc_ham``. The default value of ``1`` loads all angles in one batch. rotation_precision (int): The number of bits used to represent the precision for loading the rotation angles for basis rotation. The default value is set to ``15`` bits. select_swap_depth (int | None): A parameter of :class:`~.pennylane.estimator.templates.subroutines.QROM` used to trade off extra wires for reduced circuit depth. Defaults to :code:`None`, in which case, the ``select_swap_depth`` is set to the optimal depth that minimizes the total ``T``-gate count. wires (WiresLike | None): the wires on which the operator acts Raises: TypeError: If ``thc_ham`` is not a :class:`~pennylane.estimator.compact_hamiltonian.THCHamiltonian`. TypeError: If ``rotation_precision`` is not a positive integer. ValueError: If ``num_batches`` is not a positive integer or is greater than or equal to the number of orbitals in ``thc_ham``. ValueError: If the number of provided ``wires`` does not match the calculated ``num_wires`` required for the operation. Resources: The resources are calculated based on Figure 5 in `arXiv:2011.03494 <https://arxiv.org/abs/2011.03494>`_ and Figure 4 in `arXiv:2501.06165 <https://arxiv.org/abs/2501.06165>`_. **Example** The resources for this operation are computed using: >>> import pennylane.estimator as qre >>> thc_ham = qre.THCHamiltonian(num_orbitals=20, tensor_rank=40) >>> res = qre.estimate(qre.SelectTHC(thc_ham, rotation_precision=15)) >>> print(res) --- Resources: --- Total wires: 356 algorithmic wires: 58 allocated wires: 298 zero state: 298 any state: 0 Total gates : 3.336E+4 'Toffoli': 2.249E+3, 'CNOT': 2.344E+4, 'X': 392, 'Z': 41, 'S': 80, 'Hadamard': 7.160E+3 Let's also see how the resources change when more batches are used for the rotations: >>> res = qre.estimate(qre.SelectTHC(thc_ham, num_batches=2, rotation_precision=15)) >>> print(res) --- Resources: --- Total wires: 221 algorithmic wires: 58 allocated wires: 163 zero state: 163 any state: 0 Total gates : 3.461E+4 'Toffoli': 2.345E+3, 'CNOT': 2.438E+4, 'X': 582, 'Z': 45, 'S': 80, 'Hadamard': 7.178E+3 We can see that by batching the rotations, the number of allocated wires decreases at the cost of an increased number of Toffoli gates. """ resource_keys = {"thc_ham", "num_batches", "rotation_precision", "select_swap_depth"} def __init__( # pylint: disable=too-many-arguments self, thc_ham: THCHamiltonian, num_batches: int = 1, rotation_precision: int = 15, select_swap_depth: int | None = None, wires: WiresLike | None = None, ): if not isinstance(thc_ham, THCHamiltonian): raise TypeError( f"Unsupported Hamiltonian representation for SelectTHC." f"This method works with thc Hamiltonian, {type(thc_ham)} provided" ) if not isinstance(rotation_precision, int) or rotation_precision <= 0: raise TypeError( f"`rotation_precision` must be a positive integer, but got {rotation_precision}." ) if not isinstance(num_batches, int) or num_batches not in range(1, thc_ham.num_orbitals): raise ValueError( f"`num_batches` must be a positive integer less than the number of orbitals ({thc_ham.num_orbitals}), but got {num_batches}." ) self.thc_ham = thc_ham self.num_batches = num_batches self.rotation_precision = rotation_precision self.select_swap_depth = select_swap_depth num_orb = thc_ham.num_orbitals tensor_rank = thc_ham.tensor_rank # Based on section III D in arXiv:2011.03494 # Algorithmic wires for the walk operator, auxiliary wires are accounted for by the QROM # and SemiAdder operators. # 2*n_M wires are for \mu and \nu registers, where n_M = log_2(tensor_rank+1) # num_orb*2 for state register and 6 are flags. self.num_wires = num_orb * 2 + 2 * int(np.ceil(math.log2(tensor_rank + 1))) + 6 if wires is not None and len(Wires(wires)) != self.num_wires: raise ValueError(f"Expected {self.num_wires} wires, got {len(Wires(wires))}") super().__init__(wires=wires) @property def resource_params(self) -> dict: r"""Returns a dictionary containing the minimal information needed to compute the resources. Returns: dict: A dictionary containing the resource parameters: * thc_ham (:class:`~.pennylane.estimator.compact_hamiltonian.THCHamiltonian`): a tensor hypercontracted Hamiltonian on which this ``Select`` operator is being applied * num_batches (int): The number of batches for loading Givens rotation angles into temporary quantum registers. Must be less than the number of orbitals in ``thc_ham``. The default value of ``1`` loads all angles in one batch. * rotation_precision (int): The number of bits used to represent the precision for loading the rotation angles for basis rotation. The default value is set to ``15`` bits. * select_swap_depth (int | None): A parameter of :class:`~.pennylane.estimator.templates.subroutines.QROM` used to trade off extra wires for reduced circuit depth. Defaults to :code:`None`, in which case, the ``select_swap_depth`` is set to the optimal depth that minimizes the total ``T``-gate count. """ return { "thc_ham": self.thc_ham, "num_batches": self.num_batches, "rotation_precision": self.rotation_precision, "select_swap_depth": self.select_swap_depth, }
[docs] @classmethod def resource_rep( cls, thc_ham: THCHamiltonian, num_batches: int = 1, rotation_precision: int = 15, select_swap_depth: int | None = None, ) -> CompressedResourceOp: r"""Returns a compressed representation containing only the parameters of the Operator that are needed to compute a resource estimation. Args: thc_ham (:class:`~pennylane.estimator.compact_hamiltonian.THCHamiltonian`): A tensor hypercontracted Hamiltonian on which this ``Select`` operator is being applied. num_batches (int): The number of batches for loading Givens rotation angles into temporary quantum registers. Must be less than the number of orbitals in ``thc_ham``. The default value of ``1`` loads all angles in one batch. rotation_precision (int): The number of bits used to represent the precision for loading the rotation angles for basis rotation. The default value is set to ``15`` bits. select_swap_depth (int | None): A parameter of :class:`~.pennylane.estimator.templates.subroutines.QROM` used to trade off extra wires for reduced circuit depth. Defaults to :code:`None`, in which case, the ``select_swap_depth`` is set to the optimal depth that minimizes the total ``T``-gate count. Returns: :class:`~.pennylane.estimator.resource_operator.CompressedResourceOp`: the operator in a compressed representation """ if not isinstance(thc_ham, THCHamiltonian): raise TypeError( f"Unsupported Hamiltonian representation for SelectTHC." f"This method works with thc Hamiltonian, {type(thc_ham)} provided" ) if not isinstance(rotation_precision, int) or rotation_precision <= 0: raise TypeError( f"`rotation_precision` must be a positive integer, but got {rotation_precision}." ) if not isinstance(num_batches, int) or num_batches not in range(1, thc_ham.num_orbitals): raise ValueError( f"`num_batches` must be a positive integer less than the number of orbitals ({thc_ham.num_orbitals}), but got {num_batches}." ) num_orb = thc_ham.num_orbitals tensor_rank = thc_ham.tensor_rank num_wires = num_orb * 2 + 2 * int(np.ceil(math.log2(tensor_rank + 1))) + 6 params = { "thc_ham": thc_ham, "num_batches": num_batches, "rotation_precision": rotation_precision, "select_swap_depth": select_swap_depth, } return CompressedResourceOp(cls, num_wires, params)
[docs] @classmethod def resource_decomp( cls, thc_ham: THCHamiltonian, num_batches: int = 1, rotation_precision: int = 15, select_swap_depth: int | None = None, ) -> list[GateCount]: r"""Returns a list representing the resources of the operator. Each object represents a quantum gate and the number of times it occurs in the decomposition. .. note:: This decomposition assumes that an appropriately sized phase gradient state is available. Users should ensure that the cost of constructing this state has been accounted for. See also :class:`~.pennylane.estimator.templates.subroutines.PhaseGradient`. Args: thc_ham (:class:`~pennylane.estimator.compact_hamiltonian.THCHamiltonian`): A tensor hypercontracted Hamiltonian on which this ``Select`` operator is being applied. num_batches (int): The number of batches for loading Givens rotation angles into temporary quantum registers. Must be less than the number of orbitals in ``thc_ham``. The default value of ``1`` loads all angles in one batch. rotation_precision (int): The number of bits used to represent the precision for loading the rotation angles for basis rotation. The default value is set to ``15`` bits. select_swap_depth (int | None): A parameter of :class:`~.pennylane.estimator.templates.subroutines.QROM` used to trade off extra wires for reduced circuit depth. Defaults to :code:`None`, in which case, the ``select_swap_depth`` is set to the optimal depth that minimizes the total ``T``-gate count. Resources: The resources are calculated based on Figure 5 in `arXiv:2011.03494 <https://arxiv.org/abs/2011.03494>`_ and Figure 4 in `arXiv:2501.06165 <https://arxiv.org/abs/2501.06165>`_. Returns: list[:class:`~.pennylane.estimator.resource_operator.GateCount`]: A list of ``GateCount`` objects, where each object represents a specific quantum gate and the number of times it appears in the decomposition. """ num_orb = thc_ham.num_orbitals tensor_rank = thc_ham.tensor_rank gate_list = [] # Total select cost from Eq. 43 in arXiv:2011.03494 # 4 swaps on state registers controlled on spin qubits cswap = resource_rep(qre.CSWAP) gate_list.append(GateCount(cswap, 4 * num_orb)) restore_qrom = num_batches != 1 batched_rotations = int(np.ceil((num_orb - 1) / num_batches)) # Data output for rotations gate_list.append(Allocate(rotation_precision * batched_rotations)) # QROM to load rotation angles for both 1-body and 2-body integrals qrom_full = resource_rep( qre.QROM, { "num_bitstrings": tensor_rank + num_orb, "size_bitstring": rotation_precision * batched_rotations, "restored": restore_qrom, "select_swap_depth": select_swap_depth, }, ) gate_list.append(GateCount(qrom_full, num_batches)) # Cost for rotations by adding the rotations into the phase gradient state semiadder = resource_rep( qre.Controlled, { "base_cmpr_op": resource_rep( qre.SemiAdder, {"max_register_size": rotation_precision - 1}, ), "num_ctrl_wires": 1, "num_zero_ctrl": 0, }, ) gate_list.append(GateCount(semiadder, num_orb - 1)) # Adjoint of QROM for loading both 1-body and 2-body integrals Eq. 34 in arXiv:2011.03494 gate_list.append(GateCount(resource_rep(qre.Adjoint, {"base_cmpr_op": qrom_full}))) # Adjoint of semiadder for 1-body and 2-body integrals gate_list.append( GateCount(resource_rep(qre.Adjoint, {"base_cmpr_op": semiadder}), num_orb - 1) ) # QROM to load rotation angles for two body integrals qrom_twobody = resource_rep( qre.QROM, { "num_bitstrings": tensor_rank, "size_bitstring": rotation_precision * batched_rotations, "restored": restore_qrom, "select_swap_depth": select_swap_depth, }, ) gate_list.append(GateCount(qrom_twobody, num_batches)) # Cost for rotations by adding the rotations into the phase gradient state gate_list.append(GateCount(semiadder, num_orb - 1)) # Clifford cost for rotations h = resource_rep(qre.Hadamard) s = resource_rep(qre.S) s_dagg = resource_rep(qre.Adjoint, {"base_cmpr_op": s}) gate_list.append(GateCount(h, 4 * (num_orb))) gate_list.append(GateCount(s, 2 * num_orb)) gate_list.append(GateCount(s_dagg, 2 * num_orb)) # Adjoint of QROM for two body integrals Eq. 35 in arXiv:2011.03494 gate_list.append(GateCount(resource_rep(qre.Adjoint, {"base_cmpr_op": qrom_twobody}))) # Adjoint of semiadder for two body integrals gate_list.append( GateCount(resource_rep(qre.Adjoint, {"base_cmpr_op": semiadder}), num_orb - 1) ) # Z gate in the center of rotations gate_list.append(qre.GateCount(resource_rep(qre.Z))) cz = resource_rep(qre.CZ) gate_list.append(qre.GateCount(cz, 1)) # 1 cswap between the spin registers gate_list.append(qre.GateCount(cswap, 1)) gate_list.append(Deallocate(rotation_precision * batched_rotations)) return gate_list
[docs] @classmethod def controlled_resource_decomp( cls, num_ctrl_wires: int, num_zero_ctrl: int, target_resource_params: dict ) -> list[GateCount]: r"""Returns a list representing the resources for the controlled version of the operator. .. note:: This decomposition assumes that an appropriately sized phase gradient state is available. Users should ensure that the cost of constructing this state has been accounted for. See also :class:`~.pennylane.estimator.templates.subroutines.PhaseGradient`. Args: num_ctrl_wires (int): the number of wires the operation is controlled on num_zero_ctrl (int): the number of control wires, that are controlled when in the :math:`|0\rangle` state target_resource_params (dict): A dictionary containing the resource parameters of the target operator. Resources: The resources are calculated based on Figure 5 in `arXiv:2011.03494 <https://arxiv.org/abs/2011.03494>`_. Returns: list[:class:`~.pennylane.estimator.resource_operator.GateCount`]: A list of ``GateCount`` objects, where each object represents a specific quantum gate and the number of times it appears in the decomposition. """ thc_ham = target_resource_params["thc_ham"] rotation_precision = target_resource_params["rotation_precision"] select_swap_depth = target_resource_params["select_swap_depth"] num_batches = target_resource_params["num_batches"] num_orb = thc_ham.num_orbitals tensor_rank = thc_ham.tensor_rank gate_list = [] if num_ctrl_wires > 1: mcx = resource_rep( qre.MultiControlledX, { "num_ctrl_wires": num_ctrl_wires, "num_zero_ctrl": num_zero_ctrl, }, ) gate_list.append(Allocate(1)) gate_list.append(GateCount(mcx, 2)) # 4 swaps on state registers controlled on spin qubits cswap = resource_rep(qre.CSWAP) gate_list.append(GateCount(cswap, 4 * num_orb)) restore_qrom = num_batches != 1 batched_rotations = int(np.ceil((num_orb - 1) / num_batches)) # Data output for rotations gate_list.append(Allocate(rotation_precision * batched_rotations)) # QROM for loading rotation angles for 1-body and 2-body integrals qrom_full = resource_rep( qre.QROM, { "num_bitstrings": tensor_rank + num_orb, "size_bitstring": rotation_precision * batched_rotations, "restored": restore_qrom, "select_swap_depth": select_swap_depth, }, ) gate_list.append(GateCount(qrom_full, num_batches)) # Cost for rotations by adding the rotations into the phase gradient state semiadder = resource_rep( qre.Controlled, { "base_cmpr_op": resource_rep( qre.SemiAdder, {"max_register_size": rotation_precision - 1}, ), "num_ctrl_wires": 1, "num_zero_ctrl": 0, }, ) gate_list.append(GateCount(semiadder, num_orb - 1)) # Adjoint of QROM for 1-body and 2-body integrals Eq. 34 in arXiv:2011.03494 gate_list.append(GateCount(resource_rep(qre.Adjoint, {"base_cmpr_op": qrom_full}))) # Adjoint of semiadder for 1-body and 2-body integrals gate_list.append( GateCount(resource_rep(qre.Adjoint, {"base_cmpr_op": semiadder}), num_orb - 1) ) # QROM for loading rotation angles for two body integrals qrom_twobody = resource_rep( qre.QROM, { "num_bitstrings": tensor_rank, "size_bitstring": rotation_precision * batched_rotations, "restored": restore_qrom, "select_swap_depth": select_swap_depth, }, ) gate_list.append(GateCount(qrom_twobody, num_batches)) # Cost for rotations by adding the rotations into the phase gradient state gate_list.append(GateCount(semiadder, num_orb - 1)) # Clifford cost for rotations h = resource_rep(qre.Hadamard) s = resource_rep(qre.S) s_dagg = resource_rep(qre.Adjoint, {"base_cmpr_op": s}) gate_list.append(GateCount(h, 4 * (num_orb))) gate_list.append(GateCount(s, 2 * num_orb)) gate_list.append(GateCount(s_dagg, 2 * num_orb)) # Adjoint of QROM for two body integrals Eq. 35 in arXiv:2011.03494 gate_list.append(GateCount(resource_rep(qre.Adjoint, {"base_cmpr_op": qrom_twobody}))) # Adjoint of semiadder for two body integrals gate_list.append( GateCount(resource_rep(qre.Adjoint, {"base_cmpr_op": semiadder}), num_orb - 1) ) # Z gate in the center of rotations cz = resource_rep(qre.CZ) gate_list.append(qre.GateCount(cz, 1)) ccz = resource_rep( qre.Controlled, { "base_cmpr_op": qre.Z.resource_rep(), "num_ctrl_wires": 2, "num_zero_ctrl": 1, }, ) gate_list.append(qre.GateCount(ccz, 1)) # 1 cswap between the spin registers gate_list.append(qre.GateCount(cswap, 1)) gate_list.append(Deallocate(rotation_precision * batched_rotations)) if num_ctrl_wires > 1: gate_list.append(Deallocate(1)) elif num_zero_ctrl > 0: gate_list.append(GateCount(resource_rep(qre.X), 2 * num_zero_ctrl)) return gate_list
[docs] class SelectPauli(ResourceOperator): r"""Resource class for the ``Select`` opreation used with a Hamiltonian expressed as a linear combination of unitaries (LCU) where each unitary is a Pauli word. Args: pauli_ham (:class:`~pennylane.estimator.compact_hamiltonian.PauliHamiltonian`): A Hamiltonian expressed as a linear combination of Pauli words, over which ``Select`` is applied. wires (WiresLike | None): the wires the operation acts on Resources: The resources are based on the analysis in `Babbush et al. (2018) <https://arxiv.org/pdf/1805.03662>`_, Section III.A, 'Unary Iteration and Indexed Operations', and Figures 4, 6, and 7. Note: This implementation assumes we have access to :math:`n - 1` additional auxiliary qubits, where :math:`n = \left\lceil log_{2}(N) \right\rceil` and :math:`N` is the number of batches of unitaries to select. Raises: TypeError: If the input ``pauli_ham`` isn't an instance of :class:`~pennylane.estimator.compact_hamiltonian.PauliHamiltonian`. ValueError: if the wires provided don't match the number of wires expected by the operator .. seealso:: :class:`~.pennylane.Select` **Example** The resources for this operation are computed using: >>> import pennylane.estimator as qre >>> pauli_ham = qre.PauliHamiltonian(num_qubits=4, pauli_terms={"XY": 1, "Z": 2}) >>> select_pauli = qre.SelectPauli(pauli_ham) >>> print(qre.estimate(select_pauli)) --- Resources: --- Total wires: 7 algorithmic wires: 6 allocated wires: 1 zero state: 1 any state: 0 Total gates : 27 'Toffoli': 2, 'CNOT': 8, 'X': 4, 'Z': 1, 'S': 2, 'Hadamard': 10 """ resource_keys = {"pauli_ham"} def __init__(self, pauli_ham: PauliHamiltonian, wires: WiresLike = None) -> None: self.queue() if not isinstance(pauli_ham, PauliHamiltonian): raise TypeError( f"'pauli_ham' must be an instance of PauliHamiltonian, got {type(pauli_ham)}" ) self.pauli_ham = pauli_ham num_ctrl_wires = math.ceil(math.log2(pauli_ham.num_terms)) num_wires = pauli_ham.num_qubits + num_ctrl_wires if wires: self.wires = Wires(wires) if len(self.wires) != num_wires: raise ValueError( f"Expected {num_wires} wires ({num_ctrl_wires} control + {pauli_ham.num_qubits} target), got {len(self.wires)}." ) self.num_wires = num_wires else: self.wires = None self.num_wires = num_wires
[docs] @classmethod def resource_decomp( cls, pauli_ham: PauliHamiltonian ) -> list[GateCount]: # pylint: disable=unused-argument r"""The resources for a select implementation taking advantage of the unary iterator trick. Args: pauli_ham (:class:`~pennylane.estimator.compact_hamiltonian.PauliHamiltonian`): A Hamiltonian expressed as a linear combination of Pauli words, over which ``Select`` is applied. Resources: The resources are based on the analysis in `Babbush et al. (2018) <https://arxiv.org/pdf/1805.03662>`_, Section III.A, 'Unary Iteration and Indexed Operations', and Figures 4, 6, and 7. Note: This implementation assumes we have access to :math:`n - 1` additional auxiliary qubits, where :math:`n = \left\lceil log_{2}(N) \right\rceil` and :math:`N` is the number of batches of unitaries to select. Returns: list[:class:`~.pennylane.estimator.resource_operator.GateCount`]: A list of ``GateCount`` objects, where each object represents a specific quantum gate and the number of times it appears in the decomposition. """ gate_types = [] x = qre.X.resource_rep() cnot = qre.CNOT.resource_rep() l_elbow = resource_rep(qre.TemporaryAND) r_elbow = resource_rep(qre.Adjoint, {"base_cmpr_op": l_elbow}) pauli_terms = pauli_ham.pauli_terms if not isinstance(pauli_terms, dict): commuting_groups = pauli_terms pauli_terms = defaultdict(int) for group in commuting_groups: for pw, freq in group.items(): pauli_terms[pw] += freq num_ops = pauli_ham.num_terms work_qubits = math.ceil(math.log2(num_ops)) - 1 gate_types.append(Allocate(work_qubits)) cz = qre.CZ.resource_rep() cy = qre.CY.resource_rep() op_counts = [0, 0, 0] # cx, cy, cz counts for pw, freq in pauli_terms.items(): x_count, y_count, z_count = (0, 0, 0) for pauli_op in pw: if pauli_op == "X": x_count += 1 elif pauli_op == "Y": y_count += 1 else: z_count += 1 op_counts[0] += x_count * freq op_counts[1] += y_count * freq op_counts[2] += z_count * freq gate_types.append(GateCount(cnot, op_counts[0])) gate_types.append(GateCount(cy, op_counts[1])) gate_types.append(GateCount(cz, op_counts[2])) gate_types.append(GateCount(x, 2 * (num_ops - 1))) # conjugate 0 controlled toffolis gate_types.append(GateCount(cnot, num_ops - 1)) gate_types.append(GateCount(l_elbow, num_ops - 1)) gate_types.append(GateCount(r_elbow, num_ops - 1)) gate_types.append(Deallocate(work_qubits)) return gate_types
[docs] @classmethod def adjoint_resource_decomp(cls, target_resource_params: dict) -> list[GateCount]: r"""Returns a list representing the resources for the adjoint of the operator. Args: target_resource_params (dict): A dictionary containing the resource parameters of the target operator. Resources: Because each target operation is self-adjoint, the resources of the adjoint operation are the same as the original operation (up to some re-ordering of the application of the gates). Returns: list[:class:`~.estimator.resource_operator.GateCount`]: A list of ``GateCount`` objects, where each object represents a specific quantum gate and the number of times it appears in the decomposition. """ return [GateCount(cls.resource_rep(**target_resource_params))]
[docs] @classmethod def controlled_resource_decomp( cls, num_ctrl_wires: int, num_zero_ctrl: int, target_resource_params: dict ) -> list[GateCount]: r"""Returns a list representing the resources for a controlled version of the operator. Args: num_ctrl_wires (int): the number of qubits the operation is controlled on num_zero_ctrl (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state target_resource_params (dict): A dictionary containing the resource parameters of the target operator. Resources: The resources are based on the analysis in `Babbush et al. (2018) <https://arxiv.org/pdf/1805.03662>`_, Section III.A, 'Unary Iteration and Indexed Operations'. See Figures 4, 6, and 7. This presents the cost of a single qubit controlled ``Select`` operator. In the case of multiple control wires, we use one additional auxiliary qubit and two multi-controlled ``X`` gates. Note: This implementation assumes we have access to :math:`n` additional auxiliary qubits, where :math:`n = \left\lceil log_{2}(N) \right\rceil` and :math:`N` is the number of batches of unitaries to select. Returns: list[:class:`~.estimator.resource_operator.GateCount`]: A list of ``GateCount`` objects, where each object represents a specific quantum gate and the number of times it appears in the decomposition. """ gate_types = [] pauli_ham = target_resource_params["pauli_ham"] x = qre.X.resource_rep() cnot = qre.CNOT.resource_rep() l_elbow = resource_rep(qre.TemporaryAND) r_elbow = resource_rep(qre.Adjoint, {"base_cmpr_op": l_elbow}) if num_ctrl_wires > 1: mcx = qre.MultiControlledX.resource_rep(num_ctrl_wires, num_zero_ctrl) gate_types.append(Allocate(1)) gate_types.append(GateCount(mcx)) else: if num_zero_ctrl == 1: gate_types.append(GateCount(x, 2)) # Cost Single-Controlled Select (Unary Iterator) pauli_terms = pauli_ham.pauli_terms if not isinstance(pauli_terms, dict): commuting_groups = pauli_terms pauli_terms = defaultdict(int) for group in commuting_groups: for pw, freq in group.items(): pauli_terms[pw] += freq num_ops = pauli_ham.num_terms work_qubits = math.ceil(math.log2(num_ops)) gate_types.append(Allocate(work_qubits)) cz = qre.CZ.resource_rep() cy = qre.CY.resource_rep() op_counts = [0, 0, 0] # cx, cy, cz counts for pw, freq in pauli_terms.items(): x_count, y_count, z_count = (0, 0, 0) for pauli_op in pw: if pauli_op == "X": x_count += 1 elif pauli_op == "Y": y_count += 1 else: z_count += 1 op_counts[0] += x_count * freq op_counts[1] += y_count * freq op_counts[2] += z_count * freq gate_types.append(GateCount(cnot, op_counts[0])) gate_types.append(GateCount(cy, op_counts[1])) gate_types.append(GateCount(cz, op_counts[2])) gate_types.append(GateCount(x, 2 * (num_ops - 1))) # conjugate 0 controlled toffolis gate_types.append(GateCount(cnot, num_ops - 1)) gate_types.append(GateCount(l_elbow, num_ops - 1)) gate_types.append(GateCount(r_elbow, num_ops - 1)) gate_types.append(Deallocate(work_qubits)) # Clean up controls: if num_ctrl_wires > 1: gate_types.append(GateCount(mcx)) gate_types.append(Deallocate(1)) return gate_types
@property def resource_params(self) -> dict: r"""Returns a dictionary containing the minimal information needed to compute the resources. Returns: dict: A dictionary containing the resource parameters: * pauli_ham (:class:`~pennylane.estimator.compact_hamiltonian.PauliHamiltonian`): A Hamiltonian expressed as a linear combination of Pauli words, over which ``Select`` is applied. """ return {"pauli_ham": self.pauli_ham}
[docs] @classmethod def resource_rep(cls, pauli_ham: PauliHamiltonian) -> CompressedResourceOp: r"""Returns a compressed representation containing only the parameters of the Operator that are needed to compute the resources. Args: pauli_ham (:class:`~pennylane.estimator.compact_hamiltonian.PauliHamiltonian`): A Hamiltonian expressed as a linear combination of Pauli words, over which ``Select`` is applied. Returns: :class:`~.pennylane.estimator.resource_operator.CompressedResourceOp`: the operator in a compressed representation """ num_ctrl_wires = math.ceil(math.log2(pauli_ham.num_terms)) num_wires = pauli_ham.num_qubits + num_ctrl_wires params = {"pauli_ham": pauli_ham} return CompressedResourceOp(cls, num_wires, params)