Getting Basic Numeric Resource Estimates¶
In this tutorial, we will start exploring the quantum resource estimation tools available in Workbench and using them for benchmarking quantum programs.
Benchmarking is a critical aspect of software development, as it allows developers to identify performance bottlenecks and optimize key applications. In quantum software development, obtaining accurate benchmarks is just as vital as in classical software development. However, several factors complicate this process for quantum software:
- Quantum hardware capable of executing programs at meaningful scales (that is, programs that use hundreds of logical qubits and millions to billions of gates) does not currently exist. This means that we cannot simply run a quantum program and time how fast it completes to determine its runtime.
- There is no single, unified architecture for implementing logical quantum gates, which makes it challenging to derive an architecture-agnostic metric (such as physical runtime).
- The costs of different quantum gates vary significantly and depend on the specific architecture. This means that merely counting the number of gates does is not sufficient.
These challenges can be addressed through quantum resource estimation (QRE), a process that takes a quantum algorithm and estimates the resources required to run it on an actual fault-tolerant quantum computer under certain assumptions about its architecture.
In this tutorial, we will consider numeric resource estimation - a process that instantiates a quantum algorithm for certain inputs and produces a single set of numeric metrics for it.
The choice of metrics¶
The standard metric produced by a QRE tool is typically the number of T gates, since T gates generally dominate the runtime in fault-tolerant quantum computation. An alternative metric is the number of Toffoli gates, which is often easier to interpret algorithmically. Although both metrics are common, they omit several important details that should be considered when analyzing quantum programs:
- Rotations: In fault-tolerant quantum devices, arbitrary angle rotations are not native operations and must be synthesized, often at a substantial cost. The chosen synthesis method can significantly affect the implementation cost of these rotations. If an algorithm uses many rotations, this may indicate the need for alternative approaches or techniques such as Hamming weight phasing ⧉ to reduce the overall cost. Therefore, it is beneficial to track the number of rotations.
- Measurements: Although measurements are usually considered inexpensive (or even free) operations, they require a feedback loop from the QPU to the classical control system, which can introduce a bottleneck if the program applies other operations conditionally based on measurement outcomes. As a result, knowing the number of measurements can provide valuable insight.
Workbench tools for numeric quantum resource estimation provide the above metrics in addition to the number of T gates, the number of Toffoli gates, and the qubit highwater (the minimum number of qubits required to run the program). Additionally, the framework offers an approximation of the active volume ⧉ of the program; however, this metric should be treated with caution, as it is a cutting-edge feature that is more challenging to validate than the other metrics.
Getting basic numeric resource estimates¶
To get numeric resource estimates of a program, you start by writing this program as usual: initialize a QPU, allocate qubit registers, and apply gates/Qubricks you need to express the logic of your algorithm.
Note that programs written for the purposes of resource estimation are typically too big to be simulated using state vector simulator, and, unless they implement reversible circuits, they can't run on a bit-vector simulator either. In this case, you need to use a list of filters that does not include a simulator. The two convenient filter presets defined for this scenario are
NO_SIM_DEFAULT, which allows you to draw circuits and run resource estimation, andNO_SIM_LONG, which focuses on resource estimation without circuit drawing.
The following example implements a simple quantum Fourier transform program and displays its circuit.
from psiqworkbench import QPU, Qubits
from psiqworkbench.filter_presets import NO_SIM_DEFAULT
from psiqworkbench.qubricks import QFT
qpu = QPU(num_qubits=3, filters=NO_SIM_DEFAULT)
reg = Qubits(3, "reg", qpu)
QFT().compute(reg)
qpu.draw()
Now, you can get the numeric resource estimates for this program by calling a resource_estimator() method with the QPU as the argument and calling the resources() method on the ResourceEstimator object returned by that method.
from psiqworkbench.resource_estimation.qre import resource_estimator
resources = resource_estimator(qpu).resources()
display(resources)
{'active_volume': 381.0,
'gidney_lelbows': 0,
'gidney_relbows': 0,
'measurements': 0,
'rotations': 3,
'pprs': 0,
'ppms': 0,
't_gates': 6,
'toffs': 0,
'qubit_highwater': 3}
The resource counts are stored as a dictionary, with a separate key for each metric reported:
active_volume: PsiQuantum's active volume metric ⧉.gidney_lelbowsandgidney_relbows: left and right Gidney elbows.measurements: single-qubit measurements.rotations: arbitrary rotation gates.pprsandppms: Pauli product rotations and Pauli product measurements, typically appearing as a result of compilation for PsiQuantum's architecture.t_gates: T gates.toffs: Toffoli gates.qubit_highwater: the minimum number of qubits required to run the program.
Interpreting the metrics: rotations and T gates¶
The numbers you see above bring up the next question: where do the numbers reported by the ResourceEstimator come from? In particular, the QFT example above reports $3$ rotations and $6$ T gates, while the circuit diagram has neither.
This discrepancy arises because the QFT circuit contains gates that are not directly supported by resource estimation: controlled $S$ and $T$ gates. To account for the resources required to apply these gates, resource estimator decomposes them into gates that are supported. Workbench resource estimation tools use decompositions implemented by two filters:
clean-ladder-filter: Decomposes multi-controlled gates into single-controlled gates and left/right Gidney elbows.single-control-filter: Decomposes single-controlled gates into controlled Paulis and single-qubit gates.
The first filter will have no effect on this circuit (we'll see it in action in the next section of this tutorial), but the second filter will decompose controlled $S$ and $T$ gates into $\text{CNOT}$ gates, $T$ gates, and arbitrary rotations. To plot the decomposition as a circuit diagram, you can specify these filters upfront when creating the QPU or, alternatively, provide them as arguments to the draw() method.
qpu.draw(filters=[">>clean-ladder-filter>>", ">>single-control-filter>>"])
You can see that each of the controlled $S$ gates was decomposed into three $T$ gates, and the controlled $T$ gate - into three rotation gates. This gives us exactly the numbers produced by the resource estimator.
Note that rotation gates cannot be directly implemented in fault-tolerant quantum architectures and thus need to be approximated using sequences of other gates, including $T$ gates (see arxiv:1212.0506 ⧉ for more information). However, Workbench QRE tools don't do this synthesis by default, reporting the numbers of "raw" $T$ gates and the numbers of rotations separately.
Interpreting the metrics: multi-controlled gates¶
Now, let's take a look at another example, this time focusing on multi-controlled gates. The following code snippet runs resource estimation for a circuit that adds two numbers using the built-in NaiveAdder Qubrick. It prints two variants of the circuit: first the uncompiled circuit with multi-controlled $X$ gates and then the compiled circuit, in which these gates are decomposed into Gidney elbows and Toffoli gates.
from psiqworkbench import QPU, Qubits
from psiqworkbench.resource_estimation.qre import resource_estimator
qpu = QPU(num_qubits=10)
a = Qubits(4, "a", qpu)
b = Qubits(4, "b", qpu)
a += b
display(resource_estimator(qpu).resources())
# Push filters to draw the circuit after decomposing long Toffoli gates
qpu.box(" = ")
qpu.push_filter(">>clean-ladder-filter>>")
qpu.push_filter(">>single-control-filter>>")
# Do addition again
a += b
qpu.draw()
{'active_volume': 494,
'gidney_lelbows': 4,
'gidney_relbows': 4,
'measurements': 0,
'rotations': 0,
'pprs': 0,
'ppms': 0,
't_gates': 0,
'toffs': 6,
'qubit_highwater': 8}
Similarly to the previous example, the metrics reported by the resource estimator are based on the compiled circuit. You can see that the uncompiled circuit has only $3$ Toffoli gates, while the decompositions of controlled $X$ gates with three or more control qubits add $3$ more (and all Gidney elbows reported).
Note that the qubit_highwater metric reports the $8$ qubits allocated by the program, but not the $2$ additional qubits needed to decompose multi-controlled gates. For large-scale programs with a lot of gates that need to be decomposed, it is hard to tell how much those auxiliary qubits increase the highwater without actually compiling the entire program - that's why these numbers are resource estimates and not resource counts!
If you need to get the qubit highwater metric with higher accuracy, you can specify the filters in your QPU to decompose the program at runtime. The following example shows how to do that, and you can see that this time the qubit_highwater metric is reported as $10$.
from psiqworkbench import QPU, Qubits
from psiqworkbench.resource_estimation.qre import resource_estimator
qpu = QPU(num_qubits=10, filters=[">>clean-ladder-filter>>", ">>single-control-filter>>", ">>buffer>>"])
a = Qubits(4, "a", qpu)
b = Qubits(4, "b", qpu)
a += b
display(resource_estimator(qpu).resources())
{'active_volume': 494,
'gidney_lelbows': 4,
'gidney_relbows': 4,
'measurements': 0,
'rotations': 0,
'pprs': 0,
'ppms': 0,
't_gates': 0,
'toffs': 6,
'qubit_highwater': 10}
This approach is only limited by memory required to store the operations as they are decomposed.
Using "black box" routines for resource estimation¶
Sometimes you want to use a placeholder routine to generate numeric resource estimates without implementing all the functionality down to the gate level. This can be convenient for rapid prototyping: if a paper proposes a subroutine with complicated circuit construction and resource costs given by an analytic expression, you can use such a placeholder to see how using this subroutine would affect the cost of the larger algorithm you're working on before fully implementing it.
In Workbench, you can implement such a placeholder routine by creating a QubrickCosts object that specifies the resources consumed by the routine and then calling the add_cost_event() method of a QPU with this object as the argument.
The following code snippet shows how to instantiate and use a QubrickCosts object that requires $5^2 = 25$ Toffoli gates and $5$ rotations.
from psiqworkbench import QPU, Qubits, QubrickCosts
from psiqworkbench.filter_presets import NO_SIM_DEFAULT
from psiqworkbench.resource_estimation.qre import resource_estimator
num_qubits = 5
qpu = QPU(num_qubits=num_qubits, filters=NO_SIM_DEFAULT)
reg = Qubits(num_qubits, "reg", qpu)
cost = QubrickCosts(
toffs=num_qubits ** 2,
rotations=num_qubits
)
qpu.add_cost_event(cost, target=reg)
display(resource_estimator(qpu).resources())
{'active_volume': np.float64(1500.0),
'gidney_lelbows': 0,
'gidney_relbows': 0,
'measurements': 0,
'rotations': 5,
'pprs': 0,
'ppms': 0,
't_gates': 0,
'toffs': 25,
'qubit_highwater': 5}
You can see that the resource estimates for this code snippet include non-zero values for 'qubit_highwater' field (calculated based on the explicitly allocated qubits), 'rotations' and 'toffs' fields (provided by the QubrickCosts object), and 'active_volume' field. The latter is calculated automatically based on the other numbers, including the size of the Qubits register passed as the target argument to the add_cost_event method.
Note that this active volume estimate will typically be a significant over-estimate of the actual active volume. You can avoid this by either passing the
active_volumevalue to theQubrickCostsobject explicitly, in which case this value will be used instead, or by not passing thetargetargument to theadd_cost_eventmethod, in which case the active volume of this cost event will be $0$.
Cost events are very flexible: you can use them in functions or add them to Qubrick compute and custom uncompute methods, you can get the resource estimates of automatically generated uncompute methods or those specified by cost events in nested Qubricks, and so on. If you use a cost event to define resource requirements of a Qubrick's compute method, automatic uncomputation will mirror the elbow costs (swap resource counts of left and right Gidney elbows) and leave the rest of the fields unchanged.
The following example shows how to use cost events with Qubricks. Note that the compute method uses only left Gidney elbows, and the uncompute - only right elbows.
from psiqworkbench import Qubrick, QPU, Qubits, QubrickCosts
from psiqworkbench.filter_presets import NO_SIM_DEFAULT
from psiqworkbench.resource_estimation.qre import resource_estimator
class SimpleBlackBox(Qubrick):
"""A black box Qubrick with a resource cost but without gate implementation."""
def _compute(self, reg: Qubits):
n = reg.num_qubits
cost = QubrickCosts(
toffs=n ** 2,
gidney_lelbows=n + 2,
rotations=n,
# Pass active volume value explicitly
active_volume=2 * n ** 2
)
self.get_qc().add_cost_event(cost, target=reg)
num_qubits = 5
qpu = QPU(num_qubits=num_qubits, filters=NO_SIM_DEFAULT)
reg = Qubits(num_qubits, "reg", qpu)
bb = SimpleBlackBox()
bb.compute(reg)
print("Resource estimates for compute:")
display(resource_estimator(qpu).resources())
bb.uncompute()
print("Resource estimates for compute + uncompute:")
display(resource_estimator(qpu).resources())
Resource estimates for compute:
{'active_volume': 50,
'gidney_lelbows': 7,
'gidney_relbows': 0,
'measurements': 0,
'rotations': 5,
'pprs': 0,
'ppms': 0,
't_gates': 0,
'toffs': 25,
'qubit_highwater': 5}
Resource estimates for compute + uncompute:
{'active_volume': 100,
'gidney_lelbows': 7,
'gidney_relbows': 7,
'measurements': 0,
'rotations': 10,
'pprs': 0,
'ppms': 0,
't_gates': 0,
'toffs': 50,
'qubit_highwater': 5}
Next steps¶
In this tutorial, you've learned the basics of doing numeric resource estimation using Workbench. The next tutorial shows how to apply the numeric QRE tools to analyzing the resource requirements of different implementations of the same subroutine.