- Demos/
- Algorithms/
How to use PennyLane for Resource Estimation
How to use PennyLane for Resource Estimation
Published: January 13, 2026. Last updated: January 13, 2026.
Fault-tolerant quantum computers are on their way. However, how do we ensure that useful algorithms can actually run on them? An algorithm is hardly helpful when it cannot be executed.
This is a major challenge in quantum algorithm development, especially since we are often working at scales where simulation is no longer feasible. We therefore need to analyze our algorithms through resource estimation: getting an idea of how many resources an algorithm requires, such as logical qubits and gates. In turn, this gives us an indication of how long the algorithm will take to execute on a given quantum hardware architecture, or if it will even fit in memory to begin with.
PennyLane is here to make that process easy, with our new resource estimation module.
The estimator module leverages the latest resource estimates,
decompositions, and compilation techniques from the literature, and is designed to do so as
quickly as possible.
In this demo, we will estimate the quantum resources necessary for a simple Hamiltonian simulation workflow:
evolving the quantum state of a honeycomb lattice of spins under the
Kitaev Hamiltonian.
Estimating the Resources of existing PennyLane workflows
Let’s say you’ve already written your workflow as a PennyLane circuit.
To estimate the resources of PennyLane workflows, you can simply invoke
qre.estimate
directly on the QNode.
We demonstrate this with a \(25 \times 25\) honeycomb lattice of spins.
Here, we generate the Hamiltonian ourselves, using the
qml.spin.kitaev function,
and group the Hamiltonian terms into qubit-wise
commuting groups:
import pennylane as qml
import numpy as np
import time
n_cells = [25, 25]
kx, ky, kz = (0.5, 0.6, 0.7)
t1 = time.time()
flat_hamiltonian = qml.spin.kitaev(n_cells, coupling=np.array([kx, ky, kz]))
flat_hamiltonian.compute_grouping() # compute the qubit-wise commuting groups!
groups = []
for group_indices in flat_hamiltonian.grouping_indices:
grouped_term = qml.sum(*(flat_hamiltonian.operands[index] for index in group_indices))
groups.append(grouped_term)
grouped_hamiltonian = qml.sum(*groups)
t2 = time.time()
t_generation = t2 - t1
Here we define our circuit for Hamiltonian simulation.
num_steps = 10
order = 6
@qml.qnode(qml.device("default.qubit"))
def executable_circuit(hamiltonian, num_steps, order):
for wire in hamiltonian.wires: # uniform superposition over all basis states
qml.Hadamard(wire)
qml.TrotterProduct(hamiltonian, time=1.0, n=num_steps, order=order)
return qml.state()
Now, let’s import our quantum resource estimator.
import pennylane.estimator as qre
Just call qre.estimate
to generate the resource estimates:
t1 = time.time()
resources_exec = qre.estimate(executable_circuit)(grouped_hamiltonian, num_steps, order)
t2 = time.time()
print(f"Processing time: {(t2 - t1):.3g} seconds")
print(resources_exec)
Processing time: 4.85 seconds
--- Resources: ---
Total wires: 1250
algorithmic wires: 1250
allocated wires: 0
zero state: 0
any state: 0
Total gates : 2.972E+7
'T': 2.670E+7,
'CNOT': 1.214E+6,
'Z': 6.000E+5,
'S': 1.200E+6,
'Hadamard': 1.250E+3
Fast estimation with less information
What if we wanted to estimate the quantum resources necessary to evolve the quantum state of a \(100 \times 100\) honeycomb lattice of spins under the Kitaev Hamiltonian?
That’s 20,000 spins!
Generating such Hamiltonians becomes a bottleneck quickly as the system size increases.
However, estimator
doesn’t require detailed descriptions of Hamiltonians
for estimation; instead, we can define
resource Hamiltonians
which capture the resources required for Hamiltonian simulation
without the need to compute costly Hamiltonians.
In the particular case of the Kitaev Hamiltonian on a honeycomb lattice, we can directly compute some important quantities about the Hamiltonian. Based on the number of lattice cells \(n\) we can determine the number of qubits \(n_{q}\) the Hamiltonian acts on, the number of \(XX\)-type interactions \(n_{XX}\), the number of \(YY\)-type interactions \(n_{YY}\), and the number of \(ZZ\)-type interactions \(n_{ZZ}\).
n_cell = 100
def pauli_quantities(n_cell):
n_q = 2 * n_cell**2
n_xx = n_cell**2
n_yy = n_cell * (n_cell - 1)
n_zz = n_yy
return n_q, n_xx, n_yy, n_zz
n_q, n_xx, n_yy, n_zz = pauli_quantities(n_cell)
We can capture this information in a compact representation
using the
qre.PauliHamiltonian:
pauli_word_distribution = {"XX": n_xx, "YY": n_yy, "ZZ": n_zz}
kitaev_H = qre.PauliHamiltonian(
num_qubits=n_q,
pauli_terms=pauli_word_distribution,
)
Similarly, we can then use existing resource
operators and
templates
from the estimator module to express our circuit.
These
ResourceOperator
classes, like the PauliHamiltonian above, are designed to require minimal information
— avoiding costly compute —
while still providing trustworthy estimates.
def circuit(hamiltonian, num_steps, order):
qre.UniformStatePrep(num_states=2**n_q) # uniform superposition over all basis states
qre.TrotterPauli(hamiltonian, num_steps, order)
This circuit is purely for resource estimation and it cannot be executed for simulation. That’s what makes resource estimation so fast ⚡.
The cost of an algorithm is typically quantified by the number of logical qubits required and the
number of gates used.
While different hardware may natively support different gatesets,
the default gateset used by estimator is:
{'Hadamard', 'S', 'CNOT', 'T', 'Toffoli', 'X', 'Y', 'Z'}.
We now have a representation of our workflow using resource
operators and a resource Hamiltonian. As before, we simply call
qre.estimate
to estimate the resources:
t1 = time.time()
res = qre.estimate(circuit)(kitaev_H, num_steps, order)
t2 = time.time()
print(f"Processing time: {t2 - t1:.3g} seconds\n")
print(res)
Processing time: 0.000864 seconds
--- Resources: ---
Total wires: 2.000E+4
algorithmic wires: 20000
allocated wires: 0
zero state: 0
any state: 0
Total gates : 7.151E+8
'T': 6.556E+8,
'CNOT': 2.980E+7,
'Z': 9.900E+6,
'S': 1.980E+7,
'Hadamard': 2.000E+4
Our resource estimate was generated in the blink of an eye 👁️.
We can also analyze the resources of an individual
ResourceOperator.
This can be helpful in determining which operators in a workflow demand the most resources.
For example, let’s consider the resource estimates of
qre.TrotterPauli, and see how it changes as we provide additional
information:
resources_without_grouping = qre.estimate(qre.TrotterPauli(kitaev_H, num_steps, order))
Providing additional information can help to produce more accurate resource estimates.
In the case of our
qre.PauliHamiltonian,
we can split the terms into groups of commuting terms:
commuting_groups = [{"XX": n_xx}, {"YY": n_yy}, {"ZZ": n_zz}]
kitaev_H_with_grouping = qre.PauliHamiltonian(
num_qubits=n_q,
pauli_terms=commuting_groups,
)
resources_with_grouping = qre.estimate(
qre.TrotterPauli(kitaev_H_with_grouping, num_steps, order)
)
Let’s see how the cost of qre.TrotterPauli differs in these two cases:
# Just compare T gates:
t_count_1 = resources_without_grouping.gate_counts["T"]
t_count_2 = resources_with_grouping.gate_counts["T"]
reduction = abs((t_count_2 - t_count_1) / t_count_1)
print("--- Without grouping ---", f"\n T gate count: {t_count_1:.3E}\n")
print("--- With grouping ---", f"\n T gate count: {t_count_2:.3E}\n")
print(f"Difference: {100*reduction:.1f}% reduction")
--- Without grouping ---
T gate count: 6.556E+08
--- With grouping ---
T gate count: 4.371E+08
Difference: 33.3% reduction
By splitting our terms into groups, we’ve managed to reduce the T gate count of our
Trotterization by over 30 percent!
Changing gatesets and precision
Here are the resources for our entire circuit using the updated Hamiltonian:
res = qre.estimate(circuit)(kitaev_H_with_grouping, num_steps, order)
print(f"{res}")
--- Resources: ---
Total wires: 2.000E+4
algorithmic wires: 20000
allocated wires: 0
zero state: 0
any state: 0
Total gates : 4.867E+8
'T': 4.371E+8,
'CNOT': 1.987E+7,
'Z': 9.900E+6,
'S': 1.980E+7,
'Hadamard': 2.000E+4
We can also configure the gateset to obtain resource estimates at various levels of abstraction.
Here, we configure a high-level gateset which adds gate types such as rotations, and a low
level-gateset limited to just Hadamard, CNOT, S, and T gates.
We can see how the resources manifest at these different levels.
highlvl_gateset = {
"RX","RY","RZ",
"Toffoli",
"X","Y","Z",
"Adjoint(S)","Adjoint(T)",
"Hadamard","S","CNOT","T",
}
highlvl_res = qre.estimate(
circuit,
gate_set=highlvl_gateset,
)(kitaev_H_with_grouping, num_steps, order)
print(f"High-level resources:\n{highlvl_res}\n")
High-level resources:
--- Resources: ---
Total wires: 2.000E+4
algorithmic wires: 20000
allocated wires: 0
zero state: 0
any state: 0
Total gates : 4.962E+7
'RX': 2.510E+6,
'RY': 4.950E+6,
'Adjoint(S)': 9.900E+6,
'RZ': 2.475E+6,
'CNOT': 1.987E+7,
'S': 9.900E+6,
'Hadamard': 2.000E+4
lowlvl_gateset = {"Hadamard", "S", "CNOT", "T"}
lowlvl_res = qre.estimate(
circuit,
gate_set=lowlvl_gateset,
)(kitaev_H_with_grouping, num_steps, order)
print(f"Low-level resources:\n{lowlvl_res}")
Low-level resources:
--- Resources: ---
Total wires: 2.000E+4
algorithmic wires: 20000
allocated wires: 0
zero state: 0
any state: 0
Total gates : 4.966E+8
'T': 4.371E+8,
'CNOT': 1.987E+7,
'S': 3.960E+7,
'Hadamard': 2.000E+4
When decomposing our algorithms to a particular gateset, it is often the case that we only have some approximate decomposition of a building-block into the target gateset. For example, approximate state loading to some precision, or rotation synthesis within some precision of the rotation angle.
These approximate decompositions are accurate within some error threshold; tuning this error
threshold impacts the required resources. We can set and tune these errors using a
resource configuration: ResourceConfig.
Notice that a more precise estimate requires more T gates!
custom_rc = qre.ResourceConfig() # generate a resource configuration
rz_precisions = custom_rc.resource_op_precisions[qre.RZ]
print(f"Default setting: {rz_precisions}\n")
custom_rc.set_precision(qre.RZ, 1e-15) # customize precision
res = qre.estimate(
circuit,
gate_set=lowlvl_gateset,
config=custom_rc, # provide our custom configuration
)(kitaev_H_with_grouping, num_steps, order)
# Just compare T gates:
print("--- Lower precision (1e-9) ---", f"\n T counts: {lowlvl_res.gate_counts['T']:.3E}")
print("\n--- Higher precision (1e-15) ---", f"\n T counts: {res.gate_counts['T']:.3E}")
Default setting: {'precision': 1e-09}
--- Lower precision (1e-9) ---
T counts: 4.371E+08
--- Higher precision (1e-15) ---
T counts: 4.916E+08
The estimator module also provides functionality for
writing custom decompositions and custom resource operators.
To find out how, check out our documentation for
ResourceConfig
and ResourceOperator!
Tailored resource estimates for your needs
We can combine all of the features we have seen so far to determine the cost of Trotterized time evolution of the Kitaev Hamiltonian in our preferred setting:
t1 = time.time()
kitaev_hamiltonian = kitaev_H_with_grouping # use compact Hamiltonian with grouping
custom_gateset = lowlvl_gateset # use the low-level gateset
custom_config = qre.ResourceConfig()
custom_config.set_precision(qre.RZ, precision=1e-12) # set higher precision
resources = qre.estimate(
circuit,
gate_set = custom_gateset,
config = custom_config
)(kitaev_hamiltonian, num_steps, order)
t2 = time.time()
print(f"Processing time: {t2 - t1:.3g} seconds\n")
print(resources)
Processing time: 0.000348 seconds
--- Resources: ---
Total wires: 2.000E+4
algorithmic wires: 20000
allocated wires: 0
zero state: 0
any state: 0
Total gates : 5.239E+8
'T': 4.644E+8,
'CNOT': 1.987E+7,
'S': 3.960E+7,
'Hadamard': 2.000E+4
Comparing estimates: Full vs. Resource workflows
We’ve shown that you can estimate your workflow’s resources
using both typical PennyLane circuits, and circuits written with
ResourceOperator
classes.
Now, we’ll demonstrate that the resource estimates are consistent across both of these cases.
Let’s return to a \(25 \times 25\) unit honeycomb lattice of spins.
We’ll use estimator to make sure everything matches.
t1 = time.time()
n_cell = 25
n_q, n_xx, n_yy, n_zz = pauli_quantities(n_cell)
commuting_groups = [{"XX": n_xx}, {"YY": n_yy}, {"ZZ": n_zz}]
compact_hamiltonian = qre.PauliHamiltonian(
num_qubits = n_q,
pauli_terms = commuting_groups,
)
t2 = time.time()
t_estimation = t2 - t1
The resulting data can be easily compared for a sanity check.
print(f"Processing time for Hamiltonian generation: {(t_generation):.3g} seconds")
print("Total number of terms:", len(flat_hamiltonian.operands))
print("Total number of qubits:", len(flat_hamiltonian.wires), "\n")
print(f"Processing time for Hamiltonian estimation: {(t_estimation):.3g} seconds")
print("Total number of terms:", compact_hamiltonian.num_terms)
print("Total number of qubits:", compact_hamiltonian.num_qubits)
Processing time for Hamiltonian generation: 6.97 seconds
Total number of terms: 1825
Total number of qubits: 1250
Processing time for Hamiltonian estimation: 1.07e-05 seconds
Total number of terms: 1825
Total number of qubits: 1250
Notice how much faster it was to prepare the resource Hamiltonian for estimation!
Here’s the resource estimate from our earlier execution circuit.
print(resources_exec)
--- Resources: ---
Total wires: 1250
algorithmic wires: 1250
allocated wires: 0
zero state: 0
any state: 0
Total gates : 2.972E+7
'T': 2.670E+7,
'CNOT': 1.214E+6,
'Z': 6.000E+5,
'S': 1.200E+6,
'Hadamard': 1.250E+3
Let’s validate the results by comparing with our resource estimation circuit.
t1 = time.time()
resources_est = qre.estimate(circuit)(compact_hamiltonian, num_steps, order)
t2 = time.time()
print(f"Processing time: {(t2 - t1):.3g} seconds")
print(resources_est)
Processing time: 0.000216 seconds
--- Resources: ---
Total wires: 1250
algorithmic wires: 1250
allocated wires: 0
zero state: 0
any state: 0
Total gates : 2.972E+7
'T': 2.670E+7,
'CNOT': 1.214E+6,
'Z': 6.000E+5,
'S': 1.200E+6,
'Hadamard': 1.250E+3
The numbers check out!
Your turn!
Now that you’ve seen how powerful PennyLane’s
quantum resource estimator is,
go try it out yourself!
See how convenient it is to estimate the resources of powerful algorithms like
DQI,
XAS,
or Shor’s algorithm.
estimator includes a host of resource operators, templates, and Hamiltonians
which serve to make resource estimation a breeze for a wide variety of algorithms.
If you’re also interested in exact resource tracking of programs compiled for execution,
take a look at pennylane.specs(), which provides
resource information throughout compilation for programs compiled using pennylane.qjit().
About the authors
Jay Soni
Jay completed his BSc. in Mathematical Physics from the University of Waterloo and currently works as a Quantum Software Developer at Xanadu. Fun fact, you will often find him sipping on a Tim Horton's IceCapp while he is working.
Anton Naim Ibrahim
Physicist and Technical Product Manager. Exploring uncharted territory.
Total running time of the script: (0 minutes 11.840 seconds)