Skip to content
2 changes: 2 additions & 0 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@
work wire and :class:`pennylane.TemporaryAND` operators to reduce the resources needed.
[(#8549)](https://github.com/PennyLaneAI/pennylane/pull/8549)

* :class:`~pennylane.estimator.templates.SelectTHC` template was modified to allow trade-off between qubits and T-gates.
[(#8682)](https://github.com/PennyLaneAI/pennylane/pull/8682)
* A decomposition has been added to the adjoint of :class:`pennylane.TemporaryAND`. This decomposition relies on mid-circuit measurments and does not require any T gates.
[(#8633)](https://github.com/PennyLaneAI/pennylane/pull/8633)

Expand Down
87 changes: 77 additions & 10 deletions pennylane/estimator/templates/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ class SelectTHC(ResourceOperator):
Args:
thc_ham (:class:`~pennylane.estimator.compact_hamiltonian.THCHamiltonian`): A tensor hypercontracted
Hamiltonian on which the select operator is being applied.
batched_rotations (int | None): The maximum number of rotation angles to load simultaneously
into temporary quantum registers for processing in the Givens rotation circuits.
The default value of ``None`` loads all angles at once, where the batch size is equal to
the number of orbitals minus one.
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`
Expand Down Expand Up @@ -74,13 +78,35 @@ class SelectTHC(ResourceOperator):
'S': 80,
'Hadamard': 6.406E+3

Let's also see how the resources change when batched rotations are used:

>>> res = qre.estimate(qre.SelectTHC(thc_ham, batched_rotations=10, rotation_precision=15))
>>> print(res)
--- Resources: ---
Total wires: 227
algorithmic wires: 58
allocated wires: 169
zero state: 169
any state: 0
Total gates : 2.534E+4
'Toffoli': 2.633E+3,
'CNOT': 1.440E+4,
'X': 804.0,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
'X': 804.0,
'X': 804,

'Z': 41,
'S': 80,
'Hadamard': 7.378E+3

We can see that by using batched rotations, the number of allocated wires decreases
significantly, at the cost of an increased number of Toffoli gates.

"""

resource_keys = {"thc_ham", "rotation_precision", "select_swap_depth"}
resource_keys = {"thc_ham", "batched_rotations", "rotation_precision", "select_swap_depth"}

def __init__(
self,
thc_ham: THCHamiltonian,
batched_rotations: int | None = None,
rotation_precision: int = 15,
select_swap_depth: int | None = None,
wires: WiresLike | None = None,
Expand All @@ -97,7 +123,15 @@ def __init__(
f"`rotation_precision` must be an integer, but type {type(rotation_precision)} was provided."
)

if batched_rotations is not None and (
batched_rotations <= 0 or batched_rotations > thc_ham.num_orbitals - 1
):
raise ValueError(
f"`batched_rotations` must be a positive integer less than the number of orbitals {thc_ham.num_orbitals}, but got {batched_rotations}."
)

self.thc_ham = thc_ham
self.batched_rotations = batched_rotations
self.rotation_precision = rotation_precision
self.select_swap_depth = select_swap_depth
num_orb = thc_ham.num_orbitals
Expand Down Expand Up @@ -127,13 +161,18 @@ def resource_params(self) -> dict:
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
* batched_rotations (int | None): The maximum number of rotation angles to load simultaneously
into temporary quantum registers for processing in the Givens rotation circuits.
The default value of :code:`None` loads all angles at once, where the batch size is equal to
the number of orbitals minus one.
* 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.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,
"batched_rotations": self.batched_rotations,
"rotation_precision": self.rotation_precision,
"select_swap_depth": self.select_swap_depth,
}
Expand All @@ -142,6 +181,7 @@ def resource_params(self) -> dict:
def resource_rep(
cls,
thc_ham: THCHamiltonian,
batched_rotations: int | None = None,
rotation_precision: int = 15,
select_swap_depth: int | None = None,
) -> CompressedResourceOp:
Expand All @@ -151,6 +191,10 @@ def resource_rep(
Args:
thc_ham (:class:`~pennylane.estimator.compact_hamiltonian.THCHamiltonian`): A tensor hypercontracted
Hamiltonian on which the select operator is being applied.
batched_rotations (int | None): The maximum number of rotation angles to load simultaneously
into temporary quantum registers for processing in the Givens rotation circuits.
The default value of :code:`None` loads all angles at once, where the batch size is equal to
the number of orbitals minus one.
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.QROM`
Expand All @@ -170,6 +214,14 @@ def resource_rep(
raise TypeError(
f"`rotation_precision` must be an integer, but type {type(rotation_precision)} was provided."
)

if batched_rotations is not None and (
batched_rotations <= 0 or batched_rotations > thc_ham.num_orbitals - 1
):
raise ValueError(
f"`batched_rotations` must be a positive integer less than the number of orbitals {thc_ham.num_orbitals}, but got {batched_rotations}."
)

num_orb = thc_ham.num_orbitals
tensor_rank = thc_ham.tensor_rank

Expand All @@ -185,6 +237,7 @@ def resource_rep(
num_wires = num_orb * 2 + 2 * int(np.ceil(math.log2(tensor_rank + 1))) + 6
params = {
"thc_ham": thc_ham,
"batched_rotations": batched_rotations,
"rotation_precision": rotation_precision,
"select_swap_depth": select_swap_depth,
}
Expand All @@ -193,7 +246,8 @@ def resource_rep(
@classmethod
def resource_decomp(
cls,
thc_ham,
thc_ham: THCHamiltonian,
batched_rotations: int | None = None,
rotation_precision: int = 15,
select_swap_depth: int | None = None,
) -> list[GateCount]:
Expand All @@ -209,6 +263,10 @@ def resource_decomp(
Args:
thc_ham (:class:`~pennylane.estimator.compact_hamiltonian.THCHamiltonian`): A tensor hypercontracted
Hamiltonian on which the select operator is being applied.
batched_rotations (int | None): The maximum number of rotation angles to load simultaneously
into temporary quantum registers for processing in the Givens rotation circuits.
The default value of :code:`None` loads all angles at once, where the batch size is equal to
the number of orbitals minus one.
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.QROM`
Expand All @@ -234,20 +292,29 @@ def resource_decomp(
cswap = resource_rep(qre.CSWAP)
gate_list.append(GateCount(cswap, 4 * num_orb))

if batched_rotations is None:
batched_rotations = num_orb - 1

restore_qrom = True
if batched_rotations == num_orb - 1:
restore_qrom = False

num_givens_blocks = np.ceil((num_orb - 1) / batched_rotations)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what was causing the floats in the number of allocated qubits.

Suggested change
num_givens_blocks = np.ceil((num_orb - 1) / batched_rotations)
num_givens_blocks = int(np.ceil((num_orb - 1) / batched_rotations))


# Data output for rotations
gate_list.append(Allocate(rotation_precision * (num_orb - 1)))
gate_list.append(Allocate(rotation_precision * batched_rotations))

# 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 * (num_orb - 1),
"restored": False,
"size_bitstring": rotation_precision * batched_rotations,
"restored": restore_qrom,
"select_swap_depth": select_swap_depth,
},
)
gate_list.append(GateCount(qrom_twobody))
gate_list.append(GateCount(qrom_twobody, num_givens_blocks))

# Cost for rotations by adding the rotations into the phase gradient state
semiadder = resource_rep(
Expand All @@ -261,7 +328,7 @@ def resource_decomp(
"num_zero_ctrl": 0,
},
)
gate_list.append(GateCount(semiadder, num_orb - 1))
gate_list.append(GateCount(semiadder, batched_rotations))

# 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})))
Expand All @@ -275,12 +342,12 @@ def resource_decomp(
qre.QROM,
{
"num_bitstrings": tensor_rank,
"size_bitstring": rotation_precision * (num_orb - 1),
"restored": False,
"size_bitstring": rotation_precision * batched_rotations,
"restored": restore_qrom,
"select_swap_depth": select_swap_depth,
},
)
gate_list.append(GateCount(qrom_onebody))
gate_list.append(GateCount(qrom_onebody, num_givens_blocks))

# Cost for rotations by adding the rotations into the phase gradient state
gate_list.append(GateCount(semiadder, num_orb - 1))
Expand Down
31 changes: 17 additions & 14 deletions tests/estimator/templates/test_estimator_select.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,59 +31,62 @@ def test_wire_error(self):
qre.ControlledSequence(base=qre.SelectTHC(ch, wires=[0, 1, 2]))

@pytest.mark.parametrize(
"thc_ham, rotation_prec, selswap_depth",
"thc_ham, batched_rotations, rotation_prec, selswap_depth",
(
(qre.THCHamiltonian(58, 160), 13, 1),
(qre.THCHamiltonian(10, 50), None, None),
(qre.THCHamiltonian(4, 20), None, 2),
(qre.THCHamiltonian(58, 160), None, 13, 1),
(qre.THCHamiltonian(10, 50), None, None, None),
(qre.THCHamiltonian(4, 20), 2, None, 2),
),
)
def test_resource_params(self, thc_ham, rotation_prec, selswap_depth):
def test_resource_params(self, thc_ham, batched_rotations, rotation_prec, selswap_depth):
"""Test that the resource params for SelectTHC are correct."""
if rotation_prec:
op = qre.SelectTHC(thc_ham, rotation_prec, selswap_depth)
op = qre.SelectTHC(thc_ham, batched_rotations, rotation_prec, selswap_depth)
else:
op = qre.SelectTHC(thc_ham, select_swap_depth=selswap_depth)
op = qre.SelectTHC(thc_ham, batched_rotations=batched_rotations, select_swap_depth=selswap_depth)
rotation_prec = 15

assert op.resource_params == {
"thc_ham": thc_ham,
"batched_rotations": batched_rotations,
"rotation_precision": rotation_prec,
"select_swap_depth": selswap_depth,
}

@pytest.mark.parametrize(
"thc_ham, rotation_prec, selswap_depth, num_wires",
"thc_ham, batched_rotations, rotation_prec, selswap_depth, num_wires",
(
(qre.THCHamiltonian(58, 160), 13, 1, 138),
(qre.THCHamiltonian(10, 50), None, None, 38),
(qre.THCHamiltonian(4, 20), None, 2, 24),
(qre.THCHamiltonian(58, 160), None, 13, 1, 138),
(qre.THCHamiltonian(10, 50), None, None, None, 38),
(qre.THCHamiltonian(4, 20), 2, None, 2, 24),
),
)
def test_resource_rep(self, thc_ham, rotation_prec, selswap_depth, num_wires):
def test_resource_rep(self, thc_ham, batched_rotations, rotation_prec, selswap_depth, num_wires):
"""Test that the compressed representation for SelectTHC is correct."""
if rotation_prec:
expected = qre.CompressedResourceOp(
qre.SelectTHC,
num_wires,
{
"thc_ham": thc_ham,
"batched_rotations": batched_rotations,
"rotation_precision": rotation_prec,
"select_swap_depth": selswap_depth,
},
)
assert qre.SelectTHC.resource_rep(thc_ham, rotation_prec, selswap_depth) == expected
assert qre.SelectTHC.resource_rep(thc_ham, batched_rotations, rotation_prec, selswap_depth) == expected
else:
expected = qre.CompressedResourceOp(
qre.SelectTHC,
num_wires,
{
"thc_ham": thc_ham,
"batched_rotations": batched_rotations,
"rotation_precision": 15,
"select_swap_depth": selswap_depth,
},
)
assert qre.SelectTHC.resource_rep(thc_ham, select_swap_depth=selswap_depth) == expected
assert qre.SelectTHC.resource_rep(thc_ham, batched_rotations=batched_rotations, select_swap_depth=selswap_depth) == expected

# The Toffoli and qubit costs are compared here
# Expected number of Toffolis and wires were obtained from Eq. 44 and 46 in https://arxiv.org/abs/2011.03494
Expand Down
Loading