PennyLane
Install
Install

Contents

  1. Estimating the Resources of existing PennyLane workflows
  2. Fast estimation with less information
  3. Changing gatesets and precision
  4. Tailored resource estimates for your needs
  5. Comparing estimates: Full vs. Resource workflows
  6. Your turn!
  7. About the authors

Downloads

  • Download Python script
  • Download Notebook
  • View on GitHub
  1. Demos/
  2. Algorithms/
  3. How to use PennyLane for Resource Estimation

How to use PennyLane for Resource Estimation

Jay Soni

Jay Soni

Anton Naim Ibrahim

Anton Naim Ibrahim

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}\).

\[\begin{split}n_{q} &= 2 n^{2}, \\ n_{XX} &= n^{2}, \\ n_{YY} = n_{ZZ} &= n (n - 1), \\\end{split}\]
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 Soni

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
Anton Naim Ibrahim

Anton Naim Ibrahim

Physicist and Technical Product Manager. Exploring uncharted territory.

Total running time of the script: (0 minutes 11.840 seconds)

Share demo

Ask a question on the forum
PennyLane

PennyLane is a cross-platform Python library for quantum computing, quantum machine learning, and quantum chemistry. Built by researchers, for research. Created with ❤️ by Xanadu.

Research

  • Research
  • Performance
  • Hardware & Simulators
  • Demos
  • Quantum Compilation
  • Quantum Datasets

Education

  • Teach
  • Learn
  • Codebook
  • Coding Challenges
  • Videos
  • Glossary

Software

  • Install PennyLane
  • Features
  • Documentation
  • Catalyst Compilation Docs
  • Development Guide
  • API
  • GitHub
Stay updated with our newsletter

© Copyright 2026 | Xanadu | All rights reserved

TensorFlow, the TensorFlow logo and any related marks are trademarks of Google Inc.

Privacy Policy|Terms of Service|Cookie Policy|Code of Conduct