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
_modules/pennylane/estimator/templates/select
Download Python script
Download Notebook
View on GitHub