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

import numpy as np

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

# pylint: disable= signature-differs, arguments-differ


[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.PhaseGradient`. Args: thc_ham (:class:`~pennylane.estimator.compact_hamiltonian.THCHamiltonian`): A tensor hypercontracted Hamiltonian on which the select operator is being applied. rotation_precision (int | None): The number of bits used to represent the precision for loading the rotation angles for basis rotation. If :code:`None` is provided, the default value from the :class:`~.pennylane.estimator.resource_config.ResourceConfig` is used. 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`, which internally determines the optimal depth. wires (WiresLike | None): the wires on which the operator acts Resources: The resources are calculated based on Figure 5 in `arXiv:2011.03494 <https://arxiv.org/abs/2011.03494>`_ **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: 371 algorithmic wires: 58 allocated wires: 313 zero state: 313 any state: 0 Total gates : 1.959E+4 'Toffoli': 2.219E+3, 'CNOT': 1.058E+4, 'X': 268, 'Z': 41, 'S': 80, 'Hadamard': 6.406E+3 """ resource_keys = {"thc_ham", "rotation_precision", "select_swap_depth"} def __init__( self, thc_ham: THCHamiltonian, rotation_precision: int | None = None, 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 is None): raise TypeError( f"`rotation_precision` must be an integer, but type {type(rotation_precision)} was provided." ) self.thc_ham = thc_ham self.rotation_precision = rotation_precision self.select_swap_depth = select_swap_depth num_orb = thc_ham.num_orbitals tensor_rank = thc_ham.tensor_rank # num_orb*2 for state register, 2*log(M) for \mu and \nu registers # 6 extras are for 2 spin registers, 1 for rotation on ancilla, 1 flag for success of inequality, # 1 flag for one-body vs two-body and 1 to control swap of \mu and \nu registers. 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 the select operator is being applied * rotation_precision (int | None): The number of bits used to represent the precision for loading the rotation angles for basis rotation. If :code:`None` is provided, the default value from the :class:`~.pennylane.estimator.resource_config.ResourceConfig` is used. * select_swap_depth (int | None): A parameter of :class:`~.pennylane.estimator.templates.QROM` used to trade-off extra wires for reduced circuit depth. Defaults to :code:`None`, which internally determines the optimal depth. """ return { "thc_ham": self.thc_ham, "rotation_precision": self.rotation_precision, "select_swap_depth": self.select_swap_depth, }
[docs] @classmethod def resource_rep( cls, thc_ham: THCHamiltonian, rotation_precision: int | None = None, 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 the select operator is being applied. rotation_precision (int | None): The number of bits used to represent the precision for loading the rotation angles for basis rotation. If :code:`None` is provided, the default value from the :class:`~.pennylane.estimator.resource_config.ResourceConfig` is used. select_swap_depth (int | None): A parameter of :class:`~.pennylane.estimator.templates.QROM` used to trade-off extra wires for reduced circuit depth. Defaults to :code:`None`, which internally determines the optimal depth. 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) and rotation_precision is not None: raise TypeError( f"`rotation_precision` must be an integer, but type {type(rotation_precision)} was provided." ) num_orb = thc_ham.num_orbitals tensor_rank = thc_ham.tensor_rank # num_orb*2 for state register, 2*log(M) for \mu and \nu registers # 6 extras are for 2 spin registers, 1 for rotation on ancilla, 1 flag for success of inequality, # 1 flag for one-body vs two-body and 1 to control swap of \mu and \nu registers. num_wires = num_orb * 2 + 2 * int(np.ceil(math.log2(tensor_rank + 1))) + 6 params = { "thc_ham": thc_ham, "rotation_precision": rotation_precision, "select_swap_depth": select_swap_depth, } return CompressedResourceOp(cls, num_wires, params)
[docs] @classmethod def resource_decomp( cls, thc_ham, rotation_precision: int | None = None, 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.PhaseGradient`. Args: thc_ham (:class:`~pennylane.estimator.compact_hamiltonian.THCHamiltonian`): A tensor hypercontracted Hamiltonian on which the select operator is being applied. rotation_precision (int | None): The number of bits used to represent the precision for loading the rotation angles for basis rotation. If :code:`None` is provided, the default value from the :class:`~.pennylane.estimator.resource_config.ResourceConfig` is used. select_swap_depth (int | None): A parameter of :class:`~.pennylane.estimator.templates.QROM` used to trade-off extra wires for reduced circuit depth. Defaults to :code:`None`, which internally determines the optimal depth. Resources: The resources are calculated based on Figure 5 in `arXiv:2011.03494 <https://arxiv.org/abs/2011.03494>`_. The resources are modified to remove the control from the Select operation. 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)) # Data output for rotations gate_list.append(Allocate(rotation_precision * (num_orb - 1))) # QROM to load rotation angles for 2-body integrals qrom_twobody = resource_rep( qre.QROM, { "num_bitstrings": tensor_rank + num_orb, "size_bitstring": rotation_precision, "restored": False, "select_swap_depth": select_swap_depth, }, ) gate_list.append(GateCount(qrom_twobody)) # 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 2-body integrals Eq. 34 in arXiv:2011.03494 gate_list.append(GateCount(resource_rep(qre.Adjoint, {"base_cmpr_op": qrom_twobody}))) # Adjoint of semiadder for 2-body integrals gate_list.append( GateCount(resource_rep(qre.Adjoint, {"base_cmpr_op": semiadder}), num_orb - 1) ) # QROM to load rotation angles for one body integrals qrom_onebody = resource_rep( qre.QROM, { "num_bitstrings": tensor_rank, "size_bitstring": rotation_precision, "restored": False, "select_swap_depth": select_swap_depth, }, ) gate_list.append(GateCount(qrom_onebody)) # 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 one body integrals Eq. 35 in arXiv:2011.03494 gate_list.append(GateCount(resource_rep(qre.Adjoint, {"base_cmpr_op": qrom_onebody}))) # Adjoint of semiadder for one 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 * (num_orb - 1))) 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.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_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)) # Data output for rotations gate_list.append(Allocate(rotation_precision * (num_orb - 1))) # QROM for loading rotation angles for 2-body integrals qrom_twobody = resource_rep( qre.QROM, { "num_bitstrings": tensor_rank + num_orb, "size_bitstring": rotation_precision, "restored": False, "select_swap_depth": select_swap_depth, }, ) gate_list.append(GateCount(qrom_twobody)) # 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 2-body integrals Eq. 34 in arXiv:2011.03494 gate_list.append(GateCount(resource_rep(qre.Adjoint, {"base_cmpr_op": qrom_twobody}))) # Adjoint of semiadder for 2-body integrals gate_list.append( GateCount(resource_rep(qre.Adjoint, {"base_cmpr_op": semiadder}), num_orb - 1) ) # QROM for loading rotation angles for one body integrals qrom_onebody = resource_rep( qre.QROM, { "num_bitstrings": tensor_rank, "size_bitstring": rotation_precision, "restored": False, "select_swap_depth": select_swap_depth, }, ) gate_list.append(GateCount(qrom_onebody)) # 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 one body integrals Eq. 35 in arXiv:2011.03494 gate_list.append(GateCount(resource_rep(qre.Adjoint, {"base_cmpr_op": qrom_onebody}))) # Adjoint of semiadder for one 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 * (num_orb - 1))) 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