Source code for pennylane.labs.estimator_beta.templates.subroutines
# Copyright 2026 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 PennyLane subroutine templates."""
import math
import pennylane.labs.estimator_beta as qre
from pennylane.estimator import CompressedResourceOp, GateCount, ResourceOperator, resource_rep
from pennylane.labs.estimator_beta.wires_manager.base_classes import Allocate, Deallocate
from pennylane.math import ceil_log2
from pennylane.wires import WiresLike
# pylint: disable=unused-argument
[docs]
def selectpaulirot_controlled_resource_decomp(
num_ctrl_wires: int, num_zero_ctrl: int, target_resource_params: dict
) -> list[GateCount]:
r"""Returns a list representing the resources of the controlled version of the :class:`~pennylane.estimator.templates.SelectPauliRot` operator.
Each object in the list
represents a gate and the number of times it occurs in the circuit.
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 obtained from the construction scheme given in `Möttönen and Vartiainen
(2005), Fig 7a <https://arxiv.org/abs/quant-ph/0504100>`_. Specifically, the resources
for an :math:`n` qubit unitary are given as :math:`2^{n}` instances of the :code:`CNOT`
gate and :math:`2^{n}` instances of the controlled single qubit rotation gate (:code:`RX`,
:code:`RY` or :code:`RZ`) depending on the :code:`rot_axis`.
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_ctrl_wires_base = target_resource_params["num_ctrl_wires"]
rot_axis = target_resource_params["rot_axis"]
precision = target_resource_params["precision"]
rotation_gate_map = {
"X": qre.RX,
"Y": qre.RY,
"Z": qre.RZ,
}
gate_lst = []
gate = resource_rep(
qre.Controlled,
{
"base_cmpr_op": resource_rep(rotation_gate_map[rot_axis], {"precision": precision}),
"num_ctrl_wires": num_ctrl_wires,
"num_zero_ctrl": num_zero_ctrl,
},
)
cnot = resource_rep(qre.CNOT)
gate_lst.append(GateCount(gate, 2**num_ctrl_wires_base))
gate_lst.append(GateCount(cnot, 2**num_ctrl_wires_base))
return gate_lst
[docs]
def qft_phase_grad_resource_decomp(num_wires) -> list[GateCount]:
r"""Returns a list representing the resources of the operator. Each object in the list
represents a gate and the number of times it occurs in the circuit.
.. note::
This decomposition assumes an appropriately sized phase gradient state is available.
Users should ensure the cost of constructing such a state has been accounted for.
See also :class:`~.pennylane.estimator.templates.PhaseGradient`.
Args:
num_wires (int): the number of qubits the operation acts upon
Resources:
The resources are obtained as presented in the article
`Turning Gradients into Additions into QFTs <https://algassert.com/post/1620>`_.
Specifically, following the figure titled "8 qubit Quantum Fourier Transform with gradient shifts"
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.
"""
hadamard = resource_rep(qre.Hadamard)
swap = resource_rep(qre.SWAP)
if num_wires == 1:
return [GateCount(hadamard)]
# Use qubits from phase gradient register
phase_grad_reg = Allocate(num_wires=num_wires - 1, state="any", restored=True)
gate_types = [
phase_grad_reg,
GateCount(hadamard, num_wires),
GateCount(swap, num_wires // 2),
]
for size_reg in range(1, num_wires):
ctrl_add = qre.Controlled.resource_rep(
qre.SemiAdder.resource_rep(max_register_size=size_reg),
num_ctrl_wires=1,
num_zero_ctrl=0,
)
gate_types.append(GateCount(ctrl_add))
gate_types.append(Deallocate(allocated_register=phase_grad_reg))
return gate_types
[docs]
def aqft_resource_decomp(order, num_wires) -> list[GateCount]:
r"""Returns a list representing the resources of the operator. Each object in the list
represents a gate and the number of times it occurs in the circuit.
Args:
order (int): the maximum number of controlled phase shifts to which the operation is truncated
num_wires (int): the number of qubits the operation acts upon
Resources:
The resources are obtained from (Fig. 4) `arXiv:1803.04933 <https://arxiv.org/abs/1803.04933>`_
excluding the gate cost of preparing the phase gradient state. The phased Toffoli gates and the
classical measure-and-reset (Fig. 2) are accounted for as :code:`TemporaryAND` operations.
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.
"""
hadamard = resource_rep(qre.Hadamard)
swap = resource_rep(qre.SWAP)
cs = qre.Controlled.resource_rep(
base_cmpr_op=resource_rep(qre.S),
num_ctrl_wires=1,
num_zero_ctrl=0,
)
if order >= num_wires:
order = num_wires - 1
gate_types = [
GateCount(hadamard, num_wires),
]
if order > 1:
# Use qubits from the phase gradient register
phase_grad_register = Allocate(order - 1, state="any", restored=True)
gate_types.append(phase_grad_register)
if num_wires > 1:
gate_types.append(GateCount(cs, num_wires - 1))
for index in range(2, order):
addition_reg_size = index - 1
temp_and_register = Allocate(addition_reg_size, state="zero", restored=True)
temp_and = resource_rep(qre.TemporaryAND)
temp_and_dag = qre.Adjoint.resource_rep(temp_and)
in_place_add = qre.SemiAdder.resource_rep(addition_reg_size)
cost_iter = [
temp_and_register,
GateCount(temp_and, addition_reg_size),
GateCount(in_place_add),
GateCount(hadamard),
GateCount(temp_and_dag, addition_reg_size),
Deallocate(allocated_register=temp_and_register),
]
gate_types.extend(cost_iter)
addition_reg_size = order - 1
temp_and_register = Allocate(addition_reg_size, state="zero", restored=True)
repetitions = num_wires - order
temp_and = resource_rep(qre.TemporaryAND)
temp_and_dag = qre.Adjoint.resource_rep(temp_and)
in_place_add = qre.SemiAdder.resource_rep(addition_reg_size)
cost_iter = [
temp_and_register,
GateCount(temp_and, addition_reg_size * repetitions),
GateCount(in_place_add, repetitions),
GateCount(hadamard, repetitions),
GateCount(temp_and_dag, addition_reg_size * repetitions),
Deallocate(allocated_register=temp_and_register),
]
gate_types.extend(cost_iter)
gate_types.append(GateCount(swap, num_wires // 2))
gate_types.append(Deallocate(allocated_register=phase_grad_register))
return gate_types
[docs]
def select_thc_resource_decomp(
thc_ham: qre.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
# access the phase gradient register:
phase_grad_reg = Allocate(rotation_precision - 1, state="any", restored=True)
gate_list.append(phase_grad_reg)
# 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(math.ceil((num_orb - 1) / num_batches))
# Data output for rotations
data_reg = Allocate(rotation_precision * batched_rotations, state="zero", restored=True)
gate_list.append(data_reg)
# 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,
"borrow_qubits": 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,
"borrow_qubits": 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(allocated_register=data_reg)) # release data register
gate_list.append(Deallocate(allocated_register=phase_grad_reg)) # release phase grad register
return gate_list
[docs]
def select_thc_controlled_resource_decomp(
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 = []
# access the phase gradient register:
phase_grad_reg = Allocate(rotation_precision - 1, state="any", restored=True)
gate_list.append(phase_grad_reg)
if num_ctrl_wires > 1:
mcx = resource_rep(
qre.MultiControlledX,
{
"num_ctrl_wires": num_ctrl_wires,
"num_zero_ctrl": num_zero_ctrl,
},
)
aux_reg = Allocate(1, state="zero", restored=True)
gate_list.append(aux_reg)
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(math.ceil((num_orb - 1) / num_batches))
# Data output for rotations
data_reg = Allocate(rotation_precision * batched_rotations, state="zero", restored=True)
gate_list.append(data_reg)
# 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,
"borrow_qubits": 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,
"borrow_qubits": 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(allocated_register=data_reg)) # release data register
if num_ctrl_wires > 1:
gate_list.append(Deallocate(allocated_register=aux_reg))
elif num_zero_ctrl > 0:
gate_list.append(GateCount(resource_rep(qre.X), 2 * num_zero_ctrl))
gate_list.append(Deallocate(allocated_register=phase_grad_reg)) # release phase grad register
return gate_list
[docs]
def qrom_state_preparation_resource_decomp(
num_state_qubits: int,
positive_and_real: bool,
precision: float | None = None,
selswap_depths=1,
) -> list[GateCount]:
r"""Returns a list representing the resources of the operator. Each object in the list
represents a gate and the number of times it occurs in the circuit.
Args:
num_state_qubits (int): number of qubits required to represent the state-vector
positive_and_real (bool): Flag that the coefficients of the statevector are all real
and positive.
precision (float): The precision threshold for loading in the binary representation
of the rotation angles.
selswap_depths (int | Iterable(int) | None): A parameter of :code:`QROM`
used to trade-off extra qubits for reduced circuit depth.
Resources:
The resources for QROMStatePreparation are according to the decomposition as described
in `arXiv:0208112 <https://arxiv.org/abs/quant-ph/0208112>`_, using
:class:`~.pennylane.labs.estimator_beta.templates.subroutines.LabsQROM` to dynamically
load the rotation angles. Controlled-RY (and phase shifts) gates are used to apply all of
the rotations coherently.
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_counts = []
cry = qre.CRY.resource_rep()
phase_shift = qre.PhaseShift.resource_rep()
expected_size = num_state_qubits if positive_and_real else num_state_qubits + 1
if isinstance(selswap_depths, int) or selswap_depths is None:
selswap_depths = [selswap_depths] * expected_size
num_precision_wires = ceil_log2(math.pi / precision)
load_reg = Allocate(num_precision_wires, state="zero", restored=True)
gate_counts.append(load_reg) # allocate load register
for j in range(num_state_qubits):
num_bitstrings = 2**j
num_bit_flips = num_bitstrings * num_precision_wires // 2
gate_counts.append(
GateCount(
qre.ChangeOpBasis.resource_rep(
cmpr_compute_op=LabsQROM.resource_rep(
num_bitstrings=num_bitstrings,
size_bitstring=num_precision_wires,
num_bit_flips=num_bit_flips,
borrow_qubits=True,
select_swap_depth=selswap_depths[j],
),
cmpr_target_op=qre.Prod.resource_rep(
cmpr_factors_and_counts=((cry, num_precision_wires),),
num_wires=num_precision_wires + 1,
),
num_wires=j + num_precision_wires + 1,
),
)
)
if not positive_and_real:
gate_counts.append(
GateCount(
qre.ChangeOpBasis.resource_rep(
cmpr_compute_op=LabsQROM.resource_rep(
num_bitstrings=2**num_state_qubits,
size_bitstring=num_precision_wires,
num_bit_flips=((2**num_state_qubits) * num_precision_wires // 2),
borrow_qubits=True,
select_swap_depth=selswap_depths[-1],
),
cmpr_target_op=qre.Prod.resource_rep(
cmpr_factors_and_counts=((phase_shift, num_precision_wires),),
num_wires=num_precision_wires,
),
),
)
)
gate_counts.append(Deallocate(allocated_register=load_reg)) # free load register
return gate_counts
[docs]
def qrom_state_preparation_phase_grad_resource_decomp(
num_state_qubits: int,
positive_and_real: bool,
precision: float | None = None,
selswap_depths=1,
) -> list[GateCount]:
r"""Returns a list representing the resources of the operator. Each object in the list
represents a gate and the number of times it occurs in the circuit.
.. note::
This decomposition assumes an appropriately sized phase gradient state is available.
Users should ensure the cost of constructing such a state has been accounted for.
See also :class:`~.pennylane.estimator.PhaseGradient`.
Args:
num_state_qubits (int): number of qubits required to represent the state-vector
positive_and_real (bool): Flag that the coefficients of the statevector are all real
and positive.
precision (float): The precision threshold for loading in the binary representation
of the rotation angles.
selswap_depths (int | Iterable(int) | None): A parameter of :code:`QROM`
used to trade-off extra qubits for reduced circuit depth.
Resources:
The resources for QROMStatePreparation are according to the decomposition as described
in `arXiv:0208112 <https://arxiv.org/abs/quant-ph/0208112>`_, using
:class:`~.pennylane.labs.estimator_beta.templates.subroutines.LabsQROM` to dynamically
load the rotation angles. These rotations gates are implemented using an inplace
controlled-adder operation (see Figure 4. of `arXiv:2409.07332 <https://arxiv.org/abs/2409.07332>`_)
to phase gradient.
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_counts = []
h = qre.Hadamard.resource_rep()
s = qre.S.resource_rep()
s_dagg = qre.Adjoint.resource_rep(base_cmpr_op=s)
expected_size = num_state_qubits if positive_and_real else num_state_qubits + 1
if isinstance(selswap_depths, int) or selswap_depths is None:
selswap_depths = [selswap_depths] * expected_size
num_precision_wires = ceil_log2(math.pi / precision)
semi_adder = qre.SemiAdder.resource_rep(max_register_size=num_precision_wires)
ctrl_semi_adder = qre.Controlled.resource_rep(semi_adder, 1, 0)
load_reg = Allocate(num_precision_wires, state="zero", restored=True)
phase_grad_reg = Allocate(num_precision_wires, state="any", restored=True)
gate_counts.append(load_reg) # allocate load register
gate_counts.append(phase_grad_reg) # grab qubits from phase grad register
# map RY rotations to RZ for phase grad
gate_counts.append(GateCount(h, num_precision_wires))
gate_counts.append(GateCount(s, num_precision_wires))
for j in range(num_state_qubits):
num_bitstrings = 2**j
num_bit_flips = num_bitstrings * num_precision_wires // 2
gate_counts.append(
GateCount(
qre.ChangeOpBasis.resource_rep(
cmpr_compute_op=LabsQROM.resource_rep(
num_bitstrings=num_bitstrings,
size_bitstring=num_precision_wires,
num_bit_flips=num_bit_flips,
borrow_qubits=True,
select_swap_depth=selswap_depths[j],
),
cmpr_target_op=ctrl_semi_adder,
num_wires=j + 2 * num_precision_wires,
)
)
)
if not positive_and_real:
gate_counts.append(
GateCount(
qre.ChangeOpBasis.resource_rep(
cmpr_compute_op=LabsQROM.resource_rep(
num_bitstrings=2**num_state_qubits,
size_bitstring=num_precision_wires,
num_bit_flips=((2**num_state_qubits) * num_precision_wires // 2),
borrow_qubits=True,
select_swap_depth=selswap_depths[-1],
),
cmpr_target_op=ctrl_semi_adder,
num_wires=num_state_qubits + 2 * num_precision_wires,
)
)
)
# map RY rotations to RZ for phase grad
gate_counts.append(GateCount(h, num_precision_wires))
gate_counts.append(GateCount(s_dagg, num_precision_wires))
gate_counts.append(Deallocate(allocated_register=load_reg)) # free load register
gate_counts.append(Deallocate(allocated_register=phase_grad_reg)) # free phase grad register
return gate_counts
# pylint: disable=arguments-differ,too-many-arguments
[docs]
class LabsQROM(ResourceOperator):
r"""Resource class for the Quantum Read-Only Memory (QROM) template.
Args:
num_bitstrings (int): the number of bitstrings that are to be encoded
size_bitstring (int): the length of each bitstring
num_bit_flips (int | None): The total number of :math:`1`'s in the dataset. Defaults to
:code:`(num_bitstrings * size_bitstring) // 2`, which is half the dataset.
borrow_qubits (bool): Determine whether the auxiliary qubits should be borrowed (higher gate
cost) or freshly allocated (higher qubit cost). Defaults to :code:`True`.
select_swap_depth (int | None): A parameter :math:`\lambda` that determines
if data will be loaded in parallel by adding more rows following Figure 1.C of
`Low et al. (2024) <https://arxiv.org/pdf/1812.00954>`_. Can be :code:`None`,
:code:`1` or a positive integer power of two. Defaults to ``None``, which sets the
depth that minimizes T-gate count.
wires (WiresLike | None): The wires the operation acts on (control and target), excluding
any additional qubits allocated during the decomposition (e.g select-swap wires).
Resources:
The resources for QROM are derived from Appendix A, B from `Berry et al. (2019)
<https://arxiv.org/abs/1902.02134>`_.
* :code:`borrow_qubits=True`: Uses the borrowed qubit decomposition from Figure 4 of Appendix A in
`Berry et al. (2019) <https://arxiv.org/abs/1902.02134>`_.
* :code:`borrow_qubits=False`: Uses the clean qubit decomposition from Appendix B in
`Berry et al. (2019) <https://arxiv.org/abs/1902.02134>`_.
.. seealso::
The associated PennyLane operation :class:`~.pennylane.QROM` and the resource operator
:class:`~.pennylane.estimator.templates.subroutines.QROM`.
**Example**
The resources for this operation are computed using:
>>> import pennylane.labs.estimator_beta as qre
>>> qrom = qre.LabsQROM(
... num_bitstrings=10,
... size_bitstring=4,
... )
>>> print(qre.estimate(qrom))
--- Resources: ---
Total wires: 11
algorithmic wires: 8
allocated wires: 3
zero state: 3
any state: 0
Total gates : 86
'Toffoli': 8,
'CNOT': 36,
'X': 18,
'Hadamard': 24
"""
resource_keys = {
"num_bitstrings",
"size_bitstring",
"num_bit_flips",
"select_swap_depth",
"borrow_qubits",
}
@staticmethod
def _t_optimized_select_swap_width(num_bitstrings, size_bitstring, borrow):
pre_factor = 1 / 2 if borrow else 1
opt_width_continuous = math.sqrt(pre_factor * (num_bitstrings / size_bitstring))
if opt_width_continuous < 1:
# The continuous solution could be non-physical
w1 = w2 = 1
else:
w1 = 2 ** int(math.floor(math.log2(opt_width_continuous)))
w2 = 2 ** ceil_log2(opt_width_continuous)
def t_cost_func(w, borrow):
sel_factor, swap_factor = (2, 4) if borrow else (1, 1)
return (
sel_factor * (math.ceil(num_bitstrings / w) - 2)
+ swap_factor * (w - 1) * size_bitstring
)
if t_cost_func(w2, borrow) < t_cost_func(w1, borrow):
return w2
return w1
def __init__(
self,
num_bitstrings: int,
size_bitstring: int,
num_bit_flips: int | None = None,
borrow_qubits: bool = True,
select_swap_depth: int | None = None,
wires: WiresLike | None = None,
**kwargs,
) -> None:
if "restored" in kwargs:
raise ValueError(
"'restored' is no longer a supported argument for 'labs.estimator_beta.LabsQROM'."
"Use 'borrow_qubits = True' instead. Alternatively import 'QROM' from 'pennylane.estimator'."
)
self.borrow_qubits = borrow_qubits
self.num_bitstrings = num_bitstrings
self.size_bitstring = size_bitstring
self.num_bit_flips = num_bit_flips or (num_bitstrings * size_bitstring // 2)
self.num_control_wires = ceil_log2(num_bitstrings)
self.num_wires = size_bitstring + self.num_control_wires
if select_swap_depth is not None:
if not isinstance(select_swap_depth, int):
raise ValueError(
f"`select_swap_depth` must be None or an integer. Got {type(select_swap_depth)}"
)
exponent = int(math.log2(select_swap_depth))
if 2**exponent != select_swap_depth:
raise ValueError(
f"`select_swap_depth` must be 1 or a positive integer power of 2. Got {select_swap_depth}"
)
self.select_swap_depth = select_swap_depth
if wires is not None and len(wires) != self.num_wires:
raise ValueError(f"Expected {self.num_wires} wires, got {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:
* num_bitstrings (int): the number of bitstrings that are to be encoded
* size_bitstring (int): the length of each bitstring
* num_bit_flips (int | None): The total number of :math:`1`'s in the dataset.
Defaults to :code:`(num_bitstrings * size_bitstring) // 2`, which is half the
dataset.
* borrow_qubits (bool): Determine whether the auxiliary qubits should be borrowed
(higher gate cost) or freshly allocated (higher qubit cost). Defaults to :code:`True`.
* select_swap_depth (int | None): A parameter :math:`\lambda` that
determines if data will be loaded in parallel by adding more rows following
Figure 1.C of `Low et al. (2024) <https://arxiv.org/pdf/1812.00954>`_. Can be
:code:`None`, :code:`1` or a positive integer power of two. Defaults to None,
which sets the depth that minimizes T-gate count.
"""
return {
"num_bitstrings": self.num_bitstrings,
"size_bitstring": self.size_bitstring,
"num_bit_flips": self.num_bit_flips,
"select_swap_depth": self.select_swap_depth,
"borrow_qubits": self.borrow_qubits,
}
[docs]
@classmethod
def resource_rep(
cls,
num_bitstrings: int,
size_bitstring: int,
num_bit_flips: int | None = None,
borrow_qubits: bool = True,
select_swap_depth: int | None = None,
) -> CompressedResourceOp:
r"""Returns a compressed representation containing only the parameters of
the Operator that are needed to compute the resources.
Args:
num_bitstrings (int): the number of bitstrings that are to be encoded
size_bitstring (int): the length of each bitstring
num_bit_flips (int | None): The total number of :math:`1`'s in the dataset. Defaults to
:code:`(num_bitstrings * size_bitstring) // 2`, which is half the dataset.
borrow_qubits (bool): Determine whether the auxiliary qubits should be borrowed (higher gate
cost) or freshly allocated (higher qubit cost). Defaults to :code:`True`.
select_swap_depth (int | None): A parameter :math:`\lambda` that determines
if data will be loaded in parallel by adding more rows following Figure 1.C of
`Low et al. (2024) <https://arxiv.org/pdf/1812.00954>`_. Can be :code:`None`,
:code:`1` or a positive integer power of two. Defaults to ``None``, which sets the
depth that minimizes T-gate count.
Returns:
:class:`~.pennylane.estimator.resource_operator.CompressedResourceOp`: the operator in a compressed representation
"""
if num_bit_flips is None:
num_bit_flips = num_bitstrings * size_bitstring // 2
if select_swap_depth is not None:
if not isinstance(select_swap_depth, int):
raise ValueError(
f"`select_swap_depth` must be None or an integer. Got {type(select_swap_depth)}"
)
exponent = int(math.log2(select_swap_depth))
if 2**exponent != select_swap_depth:
raise ValueError(
f"`select_swap_depth` must be 1 or a positive integer power of 2. Got {select_swap_depth}"
)
params = {
"num_bitstrings": num_bitstrings,
"num_bit_flips": num_bit_flips,
"size_bitstring": size_bitstring,
"select_swap_depth": select_swap_depth,
"borrow_qubits": borrow_qubits,
}
num_wires = size_bitstring + ceil_log2(num_bitstrings)
return CompressedResourceOp(cls, num_wires, params)
# pylint: disable=protected-access
[docs]
@classmethod
def resource_decomp(
cls,
num_bitstrings: int,
size_bitstring: int,
num_bit_flips: int | None = None,
borrow_qubits: bool = True,
select_swap_depth: int | None = None,
) -> list[GateCount]:
r"""Returns a list of ``GateCount`` objects representing the operator's resources.
Args:
num_bitstrings (int): the number of bitstrings that are to be encoded
size_bitstring (int): the length of each bitstring
num_bit_flips (int | None): The total number of :math:`1`'s in the dataset. Defaults to
:code:`(num_bitstrings * size_bitstring) // 2`, which is half the dataset.
borrow_qubits (bool): Determine whether the auxiliary qubits should be borrowed (higher gate
cost) or freshly allocated (higher qubit cost). Defaults to :code:`True`.
select_swap_depth (int | None): A parameter :math:`\lambda` that determines
if data will be loaded in parallel by adding more rows following Figure 1.C of
`Low et al. (2024) <https://arxiv.org/pdf/1812.00954>`_. Can be :code:`None`,
:code:`1` or a positive integer power of two. Defaults to ``None``, which sets the
depth that minimizes T-gate count.
Resources:
The resources for QROM are derived from Appendix A, B from `Berry et al. (2019)
<https://arxiv.org/abs/1902.02134>`_.
* :code:`borrow_qubits=True`: Uses the borrowed qubit decomposition from Figure 4 of Appendix A in
`Berry et al. (2019) <https://arxiv.org/abs/1902.02134>`_.
* :code:`borrow_qubits=False`: Uses the clean qubit decomposition from Appendix B in
`Berry et al. (2019) <https://arxiv.org/abs/1902.02134>`_.
Note: we use the unary iterator trick to implement the ``Select``. This
implementation assumes we have access to :math:`n - 1` additional
work 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.
"""
if select_swap_depth:
max_depth = 2 ** ceil_log2(num_bitstrings)
select_swap_depth = min(max_depth, select_swap_depth) # truncate depth beyond max depth
num_bit_flips = num_bit_flips or (num_bitstrings * size_bitstring // 2)
W_opt = select_swap_depth or cls._t_optimized_select_swap_width(
num_bitstrings,
size_bitstring,
borrow_qubits,
)
L_opt = math.ceil(num_bitstrings / W_opt)
l = ceil_log2(L_opt)
gate_cost = []
if L_opt > 2:
aux_wires_select = Allocate(l - 1, state="zero", restored=True)
gate_cost.append(aux_wires_select) # Aux wires for Select (UI)
if W_opt > 1:
aux_wires_swap = Allocate(
num_wires=(W_opt - 1) * size_bitstring,
state="any" if borrow_qubits else "zero",
restored=True,
)
gate_cost.append(aux_wires_swap)
hadamard = resource_rep(qre.Hadamard)
swap_restored_prefactor = 1
select_restored_prefactor = 1
if borrow_qubits and (W_opt > 1):
gate_cost.append(GateCount(hadamard, 2 * size_bitstring))
swap_restored_prefactor = 4
select_restored_prefactor = 2
# SELECT cost:
gate_cost.extend(cls._select_cost(L_opt, num_bit_flips, select_restored_prefactor))
if L_opt > 2:
gate_cost.append(
Deallocate(allocated_register=aux_wires_select)
) # release Select aux wires
# SWAP cost:
if W_opt > 1:
gate_cost.extend(
cls._swap_cost(size_bitstring, ceil_log2(W_opt), swap_restored_prefactor)
)
if (
not borrow_qubits
): # X-axis measurement & reset (Figure 5 https://arxiv.org/abs/1902.02134)
gate_cost.append(GateCount(hadamard, (W_opt - 1) * size_bitstring))
gate_cost.append(Deallocate(allocated_register=aux_wires_swap))
return gate_cost
[docs]
@classmethod
def single_controlled_res_decomp(
cls,
num_bitstrings: int,
size_bitstring: int,
num_bit_flips: int | None = None,
select_swap_depth: int | None = None,
borrow_qubits: bool = True,
):
r"""The resource decomposition for LabsQROM controlled on a single wire.
Args:
num_bitstrings (int): the number of bitstrings that are to be encoded
size_bitstring (int): the length of each bitstring
num_bit_flips (int | None): The total number of :math:`1`'s in the dataset. Defaults to
:code:`(num_bitstrings * size_bitstring) // 2`, which is half the dataset.
borrow_qubits (bool): Determine whether the auxiliary qubits should be borrowed (higher gate
cost) or freshly allocated (higher qubit cost). Defaults to :code:`True`.
select_swap_depth (int | None): A parameter :math:`\lambda` that determines
if data will be loaded in parallel by adding more rows following Figure 1.C of
`Low et al. (2024) <https://arxiv.org/pdf/1812.00954>`_. Can be :code:`None`,
:code:`1` or a positive integer power of two. Defaults to ``None``, which sets the
depth that minimizes T-gate count.
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.
"""
if select_swap_depth:
max_depth = 2 ** ceil_log2(num_bitstrings)
select_swap_depth = min(max_depth, select_swap_depth) # truncate depth beyond max depth
W_opt = select_swap_depth or cls._t_optimized_select_swap_width(
num_bitstrings,
size_bitstring,
borrow_qubits,
)
L_opt = math.ceil(num_bitstrings / W_opt)
l = ceil_log2(L_opt)
gate_cost = []
if L_opt > 1:
aux_wires_select = Allocate(l, state="zero", restored=True)
gate_cost.append(aux_wires_select) # Aux wires for Select (UI)
if W_opt > 1:
aux_wires_swap = Allocate(
num_wires=(W_opt - 1) * size_bitstring,
state="any" if borrow_qubits else "zero",
restored=True,
)
gate_cost.append(aux_wires_swap)
hadamard = resource_rep(qre.Hadamard)
swap_restored_prefactor = 1
select_restored_prefactor = 1
if borrow_qubits and (W_opt > 1):
gate_cost.append(GateCount(hadamard, 2 * size_bitstring))
swap_restored_prefactor = 4
select_restored_prefactor = 2
# SELECT cost:
gate_cost.extend(
cls._single_ctrl_select_cost(L_opt, num_bit_flips, select_restored_prefactor)
)
if L_opt > 1:
gate_cost.append(
Deallocate(allocated_register=aux_wires_select)
) # release Select aux wires
# SWAP cost:
if W_opt > 1:
gate_cost.extend(
cls._single_ctrl_swap_cost(
size_bitstring, ceil_log2(W_opt), swap_restored_prefactor
)
)
if (
not borrow_qubits
): # X-axis measurement & reset (Figure 5 https://arxiv.org/abs/1902.02134)
gate_cost.append(
GateCount(resource_rep(qre.Hadamard), (W_opt - 1) * size_bitstring)
)
gate_cost.append(Deallocate(allocated_register=aux_wires_swap))
return gate_cost
[docs]
@classmethod
def controlled_resource_decomp(
cls,
num_ctrl_wires: int,
num_zero_ctrl: int,
target_resource_params: dict | None = None,
) -> 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 for QROM are derived from Appendix A, B from `Berry et al. (2019)
<https://arxiv.org/abs/1902.02134>`_.
* :code:`borrow_qubits=True`: Uses the borrowed qubit decomposition from Figure 4 of Appendix A in
`Berry et al. (2019) <https://arxiv.org/abs/1902.02134>`_.
* :code:`borrow_qubits=False`: Uses the clean qubit decomposition from Appendix B in
`Berry et al. (2019) <https://arxiv.org/abs/1902.02134>`_.
Note: we use the single-controlled unary iterator trick to implement the ``Select``. This
implementation assumes we have access to :math:`n` additional work qubits,
where :math:`n = \lceil \log_{2}(N) \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.
"""
num_bitstrings = target_resource_params["num_bitstrings"]
size_bitstring = target_resource_params["size_bitstring"]
num_bit_flips = target_resource_params.get("num_bit_flips", None)
select_swap_depth = target_resource_params.get("select_swap_depth", None)
borrow_qubits = target_resource_params.get("borrow_qubits", True)
gate_cost = []
if num_zero_ctrl:
x = qre.X.resource_rep()
gate_cost.append(GateCount(x, 2 * num_zero_ctrl))
if num_bit_flips is None:
num_bit_flips = (num_bitstrings * size_bitstring) // 2
single_ctrl_cost = cls.single_controlled_res_decomp(
num_bitstrings,
size_bitstring,
num_bit_flips,
select_swap_depth,
borrow_qubits,
)
if num_ctrl_wires == 1:
gate_cost.extend(single_ctrl_cost)
return gate_cost
l_elbow = resource_rep(qre.TemporaryAND)
r_elbow = resource_rep(qre.Adjoint, {"base_cmpr_op": l_elbow})
aux_reg = Allocate(num_ctrl_wires - 1, state="zero", restored=True)
gate_cost.append(aux_reg)
gate_cost.append(GateCount(l_elbow, num_ctrl_wires - 1))
gate_cost.extend(single_ctrl_cost)
gate_cost.append(GateCount(r_elbow, num_ctrl_wires - 1))
gate_cost.append(Deallocate(allocated_register=aux_reg))
return gate_cost
@classmethod
def _select_cost(
cls, num_data_blocks: int, num_bit_flips: int, repeat: int = 1
) -> list[GateCount]:
r"""The gate cost of a partial Select subroutine. This decomposition was obtained directly from
the implementation in `pennylane.templates.subroutines.select._select_resources_unary` assuming
`partial = True`.
Args:
num_data_blocks(int): The number of data blocks formed by partitioning the
total bitstrings based on select-swap depth.
num_bit_flips (int): the total number of :math:`1`'s in the dataset
repeat (int): The number of times to repeat the subroutine.
Returns:
list[GateCount]: the resource decomposition of the Select subroutine
"""
x = resource_rep(qre.X)
cnot = resource_rep(qre.CNOT)
l_elbow = resource_rep(qre.TemporaryAND)
r_elbow = resource_rep(qre.Adjoint, {"base_cmpr_op": l_elbow})
gate_cost = []
if num_data_blocks == 1:
gate_cost.append(
GateCount(
x,
num_bit_flips * repeat,
) # each unitary in the select is just an X gate to load the data
)
elif num_data_blocks == 2:
gate_cost.append(
GateCount(x, 2 * repeat),
) # for the 0-control value in the CNOTs
gate_cost.append(
GateCount(
cnot,
num_bit_flips * repeat,
) # each unitary in the select is just a CNOT
)
elif num_data_blocks / 2 ** ceil_log2(num_data_blocks) > 3 / 4:
gate_cost.append(
GateCount(x, repeat * (2 * (num_data_blocks - 3 + 1)))
) # conjugate 0-control in left-elbows + 1 extra 0-control CNOT from unary iterator decomp
gate_cost.append(
GateCount(
cnot,
repeat * num_data_blocks + repeat * num_bit_flips,
) # num CNOTs in unary iterator trick + each unitary in the select is just a CNOT
)
gate_cost.append(GateCount(l_elbow, repeat * (num_data_blocks - 3)))
gate_cost.append(GateCount(r_elbow, repeat * (num_data_blocks - 3)))
else:
gate_cost.append(
GateCount(x, repeat * (2 * (num_data_blocks - 2 + 1)))
) # conjugate 0-control in left-elbows + 1 extra 0-control CNOT from unary iterator decomp
gate_cost.append(
GateCount(
cnot,
repeat * (num_data_blocks - 2) + repeat * num_bit_flips,
) # num CNOTs in unary iterator trick + each unitary in the select is just a CNOT
)
gate_cost.append(GateCount(l_elbow, repeat * (num_data_blocks - 2)))
gate_cost.append(GateCount(r_elbow, repeat * (num_data_blocks - 2)))
return gate_cost
@classmethod
def _single_ctrl_select_cost(
cls, num_data_blocks: int, num_bit_flips: int, repeat: int = 1
) -> list[GateCount]:
r"""The decomposition of a controlled Select operation using unary iteration as
described in Figure 7. of `Babbush et al. (2018) <https://arxiv.org/abs/1805.0366>`_.
Args:
num_data_blocks(int): The number of data blocks formed by partitioning the
total bitstrings based on select-swap depth.
num_bit_flips (int): the total number of :math:`1`'s in the dataset
repeat (int): The number of times to repeat the subroutine.
Returns:
list[GateCount]: the resource decomposition of the Select subroutine
"""
x = resource_rep(qre.X)
cnot = resource_rep(qre.CNOT)
l_elbow = resource_rep(qre.TemporaryAND)
r_elbow = resource_rep(qre.Adjoint, {"base_cmpr_op": l_elbow})
gate_cost = []
if num_data_blocks == 1:
gate_cost.append(
GateCount(
cnot,
num_bit_flips * repeat,
) # each unitary in the select is just a CNOT gate to load the data
)
else: # num_data_blocks > 1
gate_cost.append(
GateCount(x, repeat * (2 * (num_data_blocks - 1)))
) # conjugate 0-control in left-elbows
gate_cost.append(
GateCount(
cnot,
repeat * ((num_data_blocks - 1) + num_bit_flips),
) # num CNOTs in unary iterator trick + each unitary in the select is just a CNOT
)
gate_cost.append(GateCount(l_elbow, repeat * (num_data_blocks - 1)))
gate_cost.append(GateCount(r_elbow, repeat * (num_data_blocks - 1)))
return gate_cost
@classmethod
def _swap_cost(
cls, register_size: int, num_swap_ctrls: int, repeat: int = 1
) -> list[GateCount]:
r"""Constructs the control-S subroutine as defined in Figure 8 of
`Berry et al. (2019) <https://arxiv.org/abs/1902.02134>`_ excluding the initial ``X`` gate.
Args:
register_size (int): The length of the bitstrings being encoded.
num_swap_ctrls (int): The number of control wires to be used for the swapping subroutine.
Should be equal to :math:`\log_{2}(\text{select_swap_depth})`.
repeat (int): The number of times to repeat the subroutine.
Returns:
list[GateCount]: the resource decomposition of the control- :math:`S` subroutine
"""
width = 2**num_swap_ctrls
ctrl_swap = resource_rep(qre.CSWAP)
return [GateCount(ctrl_swap, repeat * (width - 1) * register_size)]
@classmethod
def _swap_adj_cost(
cls, register_size: int, num_swap_ctrls: int, repeat: int = 1
) -> list[GateCount]:
r"""Constructs the control-S^adj subroutine as defined in Figure 8 to Figure 10
of `Berry et al. (2019) <https://arxiv.org/abs/1902.02134>`_ excluding the terminal ``X`` gate.
Args:
register_size (int): The length of the bitstrings being encoded.
num_swap_ctrls (int): The number of control wires to be used for the swapping subroutine.
Should be equal to :math:`\log_{2}(\text{select_swap_depth})`.
repeat (int): The number of times to repeat the subroutine.
Returns:
list[GateCount]: the resource decomposition of the control- :math:`S^{\dagger}` subroutine
"""
h = qre.resource_rep(qre.Hadamard)
cz = qre.resource_rep(qre.CZ)
cnot = qre.resource_rep(qre.CNOT)
width = 2**num_swap_ctrls - 1
return [
qre.GateCount(h, repeat * width * register_size),
qre.GateCount(cz, repeat * width * register_size),
qre.GateCount(cnot, repeat * width * register_size),
]
@classmethod
def _single_ctrl_swap_cost(
cls, register_size: int, num_swap_ctrls: int, repeat: int = 1
) -> list[GateCount]:
r"""This is a combination of the standard control-SWAP decomposition from Figure 1.b of
`Low et al. (2024) <https://arxiv.org/pdf/1812.00954>`_, with the observation that each
set of swaps acts on one of the swap-control wires at a time. We can control this decomposition
on a single qubit using a single auxiliary qubit and a pair of elbow gates for each swap-control
wires. This is because we can recycle the same auxiliary qubit for each set of elbows.
Args:
register_size (int): The length of the bitstrings being encoded.
num_swap_ctrls (int): The number of control wires to be used for the swapping subroutine.
Should be equal to :math:`\log_{2}(\text{select_swap_depth})`.
repeat (int): The number of times to repeat the subroutine.
Returns:
list[GateCount]: the resource decomposition of the control- :math:`S` subroutine
"""
width = 2**num_swap_ctrls
ctrl_swap = qre.CSWAP.resource_rep()
l_elbow = resource_rep(qre.TemporaryAND)
r_elbow = resource_rep(qre.Adjoint, {"base_cmpr_op": l_elbow})
alloc_reg = Allocate(1, state="zero", restored=True)
gate_cost = [
alloc_reg, # need one temporary qubit for l/r-elbow to control SWAP
GateCount(l_elbow, repeat * num_swap_ctrls),
GateCount(ctrl_swap, repeat * (width - 1) * register_size),
GateCount(r_elbow, repeat * num_swap_ctrls),
Deallocate(allocated_register=alloc_reg), # release temporary qubit to control SWAP
]
return gate_cost
[docs]
@classmethod
def qrom_clean_auxiliary_adjoint_resource_decomp(
cls, target_resource_params: dict
) -> list[GateCount]:
"""Returns a list representing the resources of the adjoint of the operator.
Args:
target_resource_params(dict): A dictionary containing the resource parameters of the target operator.
Resources:
This is an alternate decomposition for the adjoint of QROM which uses a measurement and phase
fixup algorithm. This decomposition requires one clean auxiliary qubit. The resources are
based on Figure 7 in Appendix C of `Berry et al. (2019) <https://arxiv.org/abs/1902.02134>`_.
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.
"""
d = target_resource_params["num_bitstrings"]
M = target_resource_params["size_bitstring"]
num_bit_flips = target_resource_params.get("num_bit_flips", None)
if num_bit_flips is None:
num_bit_flips = (d * M) // 2
# Set optimal Select-Swap depth
k_approx = math.sqrt(
d
) # minimizes Toffoli cost Appendix C. https://arxiv.org/abs/1902.02134
k = 2 ** round(math.log2(k_approx)) # must be a power of 2
gate_lst = []
x = resource_rep(qre.X)
had = qre.resource_rep(qre.Hadamard)
# Measure + Reset output register in the X-basis:
gate_lst.append(GateCount(had, M))
if M < k:
aux_reg = Allocate(
k - M, state="zero", restored=True
) # we can re-use the M output qubits
gate_lst.append(aux_reg)
new_address_size = int(math.ceil(d / k))
swap_ctrl_register_size = int(math.log2(k))
# Apply phase fixup (Fig. 6 Appendix C. https://arxiv.org/abs/1902.02134)
gate_lst.append(GateCount(x))
gate_lst.extend(cls._swap_cost(register_size=1, num_swap_ctrls=swap_ctrl_register_size))
gate_lst.append(GateCount(had, k))
gate_lst.extend(cls._select_cost(new_address_size, num_bit_flips))
gate_lst.append(GateCount(had, k))
gate_lst.extend(cls._swap_adj_cost(register_size=1, num_swap_ctrls=swap_ctrl_register_size))
gate_lst.append(GateCount(x))
if M < k:
gate_lst.append(Deallocate(allocated_register=aux_reg)) # all qubits restored
return gate_lst
[docs]
@classmethod
def qrom_dirty_auxiliary_adjoint_resource_decomp(
cls, target_resource_params: dict
) -> list[GateCount]:
"""Returns a list representing the resources of the adjoint of the operator.
Args:
target_resource_params(dict): A dictionary containing the resource parameters of the target operator.
Resources:
This is an alternate decomposition for the adjoint of QROM which uses a measurement and phase
fixup algorithm. This decomposition requires one borrowed auxiliary qubit. The resources are
based on Figure 7 in Appendix C of `Berry et al. (2019) <https://arxiv.org/abs/1902.02134>`_.
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.
"""
d = target_resource_params["num_bitstrings"]
M = target_resource_params["size_bitstring"]
num_bit_flips = target_resource_params.get("num_bit_flips", None)
if num_bit_flips is None:
num_bit_flips = (d * M) // 2
# Set optimal Select-Swap depth
k_approx = math.sqrt(
d / 2
) # minimizes Toffoli cost Appendix C. https://arxiv.org/abs/1902.02134
k = 2 ** round(math.log2(k_approx)) # must be a power of 2
gate_lst = []
z = resource_rep(qre.Z)
had = qre.resource_rep(qre.Hadamard)
# Measure + Reset output register in the X-basis:
gate_lst.append(GateCount(had, M))
if M < k:
aux_reg = Allocate(
k - M, state="any", restored=True
) # we can re-use the M output qubits
gate_lst.append(aux_reg)
new_address_size = int(math.ceil(d / k))
swap_ctrl_register_size = int(math.log2(k))
t = cls._select_cost(new_address_size, num_bit_flips, repeat=2)
s = cls._swap_cost(register_size=1, num_swap_ctrls=swap_ctrl_register_size, repeat=4)
# Apply phase fixup (Fig. 7 Appendix C. https://arxiv.org/abs/1902.02134)
gate_lst.append(GateCount(z, 2))
gate_lst.append(GateCount(had, 2))
gate_lst.extend(s)
gate_lst.extend(t)
if M < k:
gate_lst.append(Deallocate(allocated_register=aux_reg)) # all qubits restored
return gate_lst
[docs]
@classmethod
def adjoint_resource_decomp(cls, target_resource_params: dict | None = None) -> list[GateCount]:
r"""Returns a list representing the resources of the adjoint of the operator.
Args:
target_resource_params(dict): A dictionary containing the resource parameters of the target operator.
Resources:
This is an alternate decomposition for the adjoint of QROM which uses a measurement and phase
fixup algorithm. This decomposition requires one clean auxiliary qubit. The resources are
based on Figure 7 in Appendix C of `Berry et al. (2019) <https://arxiv.org/abs/1902.02134>`_.
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.
"""
return cls.qrom_clean_auxiliary_adjoint_resource_decomp(target_resource_params)
[docs]
@staticmethod
def tracking_name(
num_bitstrings: int,
size_bitstring: int,
num_bit_flips: int | None = None,
borrow_qubits: bool = True,
select_swap_depth: int | None = None,
):
return "QROM"
_modules/pennylane/labs/estimator_beta/templates/subroutines
Download Python script
Download Notebook
View on GitHub