Resource Estimation#
Qrisp provides utilities to estimate resources of a quantum circuit
represented by a jaspr. These functions analyze the structure of the
circuit.
Gate count#
- count_ops(meas_behavior: str | Callable) Callable[source]#
Decorator to determine resources of large scale quantum computations. This decorator compiles the given Jasp-compatible function into a classical function computing the amount of each gates required. The decorated function will return a dictionary containing the operation counts.
For many algorithms including classical feedback, the result of the measurements can heavily influence the required resources. To reflect this, users can specify the behavior of measurements during the computation of resources. The following strategies are available:
"0"- computes the resource as if measurements always return 0"1"- computes the resource as if measurements always return 1callable - allows the user to specify a random number generator (see examples)
For more details on how the callable option can be used, consult the examples section.
Finally it is also possible to call the Qrisp simulator to determine measurement behavior by providing
"sim". This is of course much less scalable but in particular for algorithms involving repeat-until-success components, a necessary evil.Note that the
"sim"option might return non-deterministic results, while the other methods do.Warning
It is currently not possible to estimate programs, which include a kernelized function.
- Parameters:
- meas_behaviorstr or callable
A string or callable indicating the behavior of the resource computation when measurements are performed. Available strings are
"0","1", and"sim".
- Returns:
- resource_estimation decoratorCallable
A decorator, producing a function to computed the required resources.
Examples
We compute the resources required to perform a large scale integer multiplication.
from qrisp import count_ops, QuantumFloat, measure @count_ops(meas_behavior = "0") def main(i): a = QuantumFloat(i) b = QuantumFloat(i) c = a*b return measure(c) print(main(5)) # {'s': 45, 'x': 22, 't_dg': 98, 'cx': 510, 't': 96, 'h': 139, 'measure': 55} print(main(5000)) # {'t': 751506, 'h': 1127254, 'x': 2002, 's': 375750, 't_dg': 751508, 'cx': 4629255, 'measure': 752500}
Note that even though the second computation contains more than 800 million gates, determining the resources takes less than 200ms, highlighting the scalability features of the Jasp infrastructure.
Modifying the measurement behavior via a random number generator
To specify the behavior, we specify an RNG function (for more details on what that means please check the Jax documentation. This RNG takes as input a “key” and returns a boolean value. In this case, the return value will be uniformly distributed among True and False.
from jax import random import jax.numpy as jnp from qrisp import QuantumFloat, measure, control, count_ops, x # Returns a uniformly distributed boolean def meas_behavior(key): return jnp.bool(random.randint(key, (1,), 0,1)[0]) @count_ops(meas_behavior = meas_behavior) def main(i): qv = QuantumFloat(2) meas_res = measure(qv) with control(meas_res == i): x(qv) return measure(qv)
This script executes two measurements and based on the measurement outcome executes two X gates. We can now execute this resource computation with different values of
ito see, which measurements returnTruewith our given random-number generator (recall that this way of specifying the measurement behavior is fully deterministic).print(main(0)) # Yields: {'measure': 4, 'x': 2} print(main(1)) # Yields: {'measure': 4} print(main(2)) # Yields: {'measure': 4} print(main(3)) # Yields: {'measure': 4}
From this we conclude that our RNG returned 0 for both of the initial measurements.
For some algorithms (such as Repeat-Until-Success) sampling the measurement result from a simple distribution won’t cut it because the required ressource can be heavily influenced by measurement outcomes. For this matter it is also possible to perform a full simulation. Note that this simulation is no longer deterministic.
@count_ops(meas_behavior = "sim") def main(i): qv = QuantumFloat(2) meas_res = measure(qv) with control(meas_res == i): x(qv) return measure(qv) print(main(0)) {'measure': 4, 'x': 2} print(main(1)) {'measure': 4}
Circuit depth#
- depth(meas_behavior: str | Callable, max_qubits: int = 1024) Callable[source]#
Decorator to determine the depth of large scale quantum computations.
This decorator compiles the given Jasp-compatible function into a classical function computing the circuit depth required. The decorated function returns an integer indicating the depth of the quantum computation.
The depth is computed by tracking, for each qubit, the time at which it becomes available again after an operation. Multi-qubit gates increase the depth of all qubits they act on to the same value.
- Parameters:
- meas_behaviorstr or callable
A string or callable indicating the behavior of the resource computation when measurements are performed. Available strings are
"0"and"1". A callable must take a JAX PRNG key as input and return a boolean.- max_qubitsint, optional
The maximum number of qubits supported for depth computation. Default is 1024.
- Returns:
- depth decoratorCallable
A decorator producing a function that computes the depth required.
Examples
Let’s consider a simple circuit:
from qrisp import * @depth(meas_behavior="0") def circuit(n): qv = QuantumFloat(n) h(qv[0]) h(qv[1]) cx(qv[0], qv[1]) h(qv[0]) print(circuit(2)) # Output: 3
The first two Hadamards run in parallel (depth 1), the CNOT increases depth to 2, and the final Hadamard gives depth 3.
Now, consider a circuit with measurement and classical control:
@depth(meas_behavior="0") def circuit(n): qv = QuantumFloat(n) m = measure(qv[0]) with control(m == 0): h(qv[0]) x(qv[1]) h(qv[0]) with control(m == 1): cx(qv[0], qv[1]) h(qv[0]) x(qv[0]) print(circuit(2)) # Output: 2
The same circuit with
meas_behavior="1"yields a depth of 3, because a different branch of the computation is taken.Macro-gates and gate definitions
If a gate has a
definition(for example a Toffoli gate implemented as a sequence of simpler gates), the transpile method is applied to the definition to determine the depth of the macro-gate.Note
Computing depth requires tracking qubit dependencies. As a result, compilation time for the depth metric can be noticeably slower for large circuits compared to
count_ops. This will be improved in future versions. However, the scalability offered by Jasp after the initial compilation is not affected.Note
The
max_qubitsparameter sets an upper limit on the number of qubits that can be handled for depth computation. This is necessary as JAX requires static shapes for JIT compilation. The default value of 1024 can be adjusted based on the expected number of qubits in the circuits to be analyzed.Warning
It is currently not possible to estimate programs, which include a kernelized function.
Warning
The depth metric an experimental feature and may not behave as expected in certain edge cases.
The memory management operations
resetanddeleteare currently ignored. Qubits freed by these calls still count toward themax_qubitslimit.This metric can currently handle the slice operation correctly only when the lower bound of the slice is strictly smaller than the upper bound.
Number of qubits#
- num_qubits(meas_behavior: str | Callable, max_allocations: int = 1000) Callable[source]#
Decorator to track qubit allocation and deallocation events during a quantum computation.
This decorator compiles a Jasp-compatible quantum function into a resource-analysis function that tracks qubit allocation and deallocation events throughout the computation.
An internal allocation counter is updated as follows:
increased whenever qubits are allocated (e.g., via
QuantumVariablecreation),decreased whenever qubits are explicitly deleted (e.g., via
qv.delete()),
The decorated function returns a dictionary containing information about all allocation and deallocation events.
These are:
total_allocated: the total number of qubits allocated during the computation.total_deallocated: the total number of qubits deallocated during the computation.peak_allocations: the maximum number of qubits allocated at any point during the computation.finally_allocated: the number of qubits still allocated at the end of the computation.
See the examples below for more details on how to interpret these values.
- Parameters:
- meas_behaviorstr or callable
A string or callable indicating the behavior of the resource computation when measurements are performed. Available strings are
"0"and"1". A callable must take a JAX PRNG key as input and return a boolean.- max_allocationsint, optional
The maximum number of allocation/deallocation events supported for tracking. Default is 1000. This is necessary as JAX requires static shapes for JIT compilation.
- Returns:
- Callable
A decorator producing a function that returns a dictionary containing aggregated statistics about allocation and deallocation events during the computation.
Examples
Let’s consider a simple circuit in which the number of allocated qubits depends on the measurement outcome:
from qrisp import * @num_qubits(meas_behavior="0") def circuit(n1, n2, n3): qv = QuantumFloat(n1) m = measure(qv[0]) with control(m == 0): qv2 = QuantumFloat(n2) h(qv2[0]) with control(m == 1): qv3 = QuantumFloat(n3) h(qv3[0]) print(circuit(2, 3, 4)) # Output: # {'total_allocated': 5, 'total_deallocated': 0, # 'peak_allocations': 5, 'finally_allocated': 5}
Here, the measurement of the first qubit determines whether we allocate 3 or 4 additional qubits. The output dictionary contains information about the total number of allocated qubits (5), the total number of deallocated qubits (0), the peak number of allocated qubits at any point during the computation (5), and the number of qubits still allocated at the end of the computation (5). If we change the measurement behavior to
"1", we get a different output.Note that deallocation affects the final count:
@num_qubits(meas_behavior="0") def circuit(n): qv = QuantumFloat(2 * n) h(qv[0]) qv.delete() qv = QuantumFloat(n) h(qv[0]) print(circuit(4)) # Output: # {'total_allocated': 12, 'total_deallocated': 8, # 'peak_allocations': 8, 'finally_allocated': 4}
Here, we first allocate 8 qubits, then deallocate them, and finally allocate 4 more qubits.
Let’s see a final example with branching and deallocation:
from qrisp import * @num_qubits(meas_behavior="1") def circuit(num_qubits_input): list_of_qvs = [] for i in range(2): qv = QuantumFloat(num_qubits_input) h(qv[i]) list_of_qvs.append(qv) qv_2 = QuantumFloat(1) h(qv_2[0]) m = measure(qv_2[0]) qv_2.delete() with control(m == 1): qv4 = QuantumFloat(10) h(qv4[0]) qv4.delete() for i in range(2): list_of_qvs[i].delete() print(circuit(8)) # Output: # {'total_allocated': 27, 'total_deallocated': 27, # 'peak_allocations': 26, 'finally_allocated': 0}
In this example, the peak number of allocated qubits is different from the total allocated because
qv_2is deleted before the subsequent allocation ofqv4. The final number of allocated qubits is 0 because all allocated qubits are eventually deallocated.Warning
Programs that include a kernelized function cannot currently be analyzed.