Basic example¶
In this tutorial our goals are to learn:
- How to use
bartiqto implement a quantum algorithm from a paper. - How to obtain resource estimates for that algorithm.
- What the most important concepts used in
bartiqare.
NOTE:
This tutorial, as well as all the other tutorials, has been written as a jupyter notebook.
If you're reading it online, you can either keep reading, or go to docs/tutorials to explore them in a more interactive way!
Before we start implementing some real algorithms, let's consider the following simple routine:

In bartiq we call our main object Routine – both the whole algorithm here, as well as each operation will be a Routine.
So what do we know about the routines from the picture above?
- Our main routine is called "my algorithm"
- It consists of two subroutines: "A" and "B".
- It takes in a register of size "n".
How do we express this in bartiq?
We do that using the QREF format ⧉ – a format for expressing algorithms that we developed with QREs in mind. So let's write our first routine:
my_algorithm = {
"name": "my_algorithm",
"type": None,
"ports": [
{"name": "in", "direction": "input", "size": "n"},
{"name": "out", "direction": "output", "size": None},
],
}
What do we have here?
name: name of the routinetype: in this case we don't define the type, but in more complex algorithms you might want to add types, such as "basic_gate" or "comparator".ports: ports define the interface of the routine. The size of the input port is equal tonand in general, we won't know the size of the output port until we perform the compilation.
What are we missing? Children.
Before we add them to the main routine we need to define them though.
routine_a = {
"name": "A",
"type": None,
"ports": [
{"name": "in", "direction": "input", "size": "n_a"},
{"name": "out", "direction": "output", "size": "2*n_a"},
],
}
Notice, that the sizes of input and output ports don't need to match. Here we defined that the size of the output port is twice the size of the input.
routine_b = {
"name": "B",
"type": None,
"ports": [
{"name": "in", "direction": "input", "size": "n_b"},
# "y" will be defined in the next step
{"name": "out", "direction": "output", "size": "n_b + y"},
],
}
We will need to know how much each subroutine costs if we want to run the resource estimation. In fault-tolerant quantum computation a common metric of interest is an algorithm's T-gate count. (T-gates are a particular quantum gate which are typically more expensive to implement than other quantum gates, and so are commonly used as the standard metric for an algorithm's computational cost.)
In this example, let's say that routine A costs 2*n_a + x T gates and routine B costs n_b*ceil(log_2(n_b)) * y T-gates, where x and y are some arbitrary paremeters.
Knowing T-gate costs and sizes of parameters, we can now visualize subroutines A and B like this:

This will require adding two new fields to the dictionaries defining A and B respectively:
# Define T-gate counts for routine a
routine_a["input_params"] = ["x"]
routine_a["resources"] = [
{"name": "T_gates", "type": "additive", "value": "2*n_a + x"}
]
# Define T-gate counts for routine b
routine_b["input_params"] = ["y"]
routine_b["resources"] = [
{"name": "T_gates", "type": "additive", "value": "n_b*ceil(log_2(n_b)) * y"}
]
As you can see we added two new fields to our dictionaries:
input_params: which defines the variables used by the routine's resource expressions.resources: which defines the resource costs for our routine. As you can see resources have the following fields:name: name of the resourcetype:bartiqallows for the following types:additive,multiplicative,qubitsandother.value: expression (or numeric value) defining the cost.
Now that routine_a and routine_b are complete, we can add the missing components to my_algorithm:
my_algorithm["children"] = [routine_a, routine_b]
my_algorithm["connections"] = [
{"source": "in", "target": "A.in"},
{"source": "A.out", "target": "B.in"},
{"source": "B.out", "target": "out"},
]
my_algorithm["input_params"] = ["z"]
my_algorithm["linked_params"] = [{"source": "z", "targets": ["A.x", "B.y"]}]
The new things we have here are:
connections: defines how routines are connected via their ports. Each connection has source and target.children: defines a routine's subroutines.linked_params: defines how input parameters used by the parent are linked to the parameters of children. In this case, it specifies that the input parameterzshould be passed asxto the subroutineAand asytoB. Note that we don't need to pass information aboutn,n_aandn_b, as this information gets passed through the connections.
The last step is just a formality to indicate which version of QREF schema we use:
my_algorithm_qref = {"version": "v1", "program": my_algorithm}
So, is there an intuitive way to understand what my algorithm looks like and how the resources are used in each routine? You can use the visualization tool from QREF ⧉ to plot the hierarchical Directed Acyclic Graph (DAG) of the algorithm you wrote.
NOTE:
To use the qref ⧉ rendering tool in Jupyter Notebook, ensure the Graphviz software is installed on your OS and that its executables are included in your system variables. For installation instructions, please refer to the Graphviz download page ⧉.
Then, run:
from qref.experimental.rendering import to_graphviz
# Convert the qref format to Graphviz object
gv_object = to_graphviz(my_algorithm_qref)
# Render the Graphviz object to a PNG file
gv_object.render("my_algorithm", format="png")
# Render the Graphviz object in the notebook
gv_object
From this graph, you can find both subroutines from the original algorithm, along with their name, andports. It provides a general idea of the connectivity between subroutines in the algorithm and shows how information is stored.
Now we can translate our algorithm into a proper bartiq routine and see what's the total cost of my_algorithm.
Compilation¶
Below you can find depiction of my_algorithm.

We can create bartiq Routine from QREF definition by simply running:
from bartiq.integrations import qref_to_bartiq
uncompiled_routine = qref_to_bartiq(my_algorithm_qref)
What does "uncompiled" means here?
It means that all the costs and register sizes are expressed using local variables (as in the picture above). What does it mean? Look at this:
uncompiled_routine.children["A"].resources
{'T_gates': <Resource name="T_gates" value="2*n_a + x">}
The cost of A is still expressed in terms of its own "local" variables, n_a and x. Information that we included in linked_params has not yet been propagated into A.
We also don't know yet what's the size of the output port:
uncompiled_routine.ports["out"]
Port(my_algorithm.#out, size=None, output)
Most importantly, we don't know what is the total cost of the algorithm:
uncompiled_routine.resources
{}
So what we want to do, is to get to the following picture:

You can compare it with the previous picture and see how "local" variables have been replaced with "global" ones.
We do this with the following command:
from bartiq import compile_routine
compiled_routine = compile_routine(uncompiled_routine)
/Users/kjalowiecki/Projects/bartiq/src/bartiq/compilation/_compile.py:117: UserWarning: Found the following issues with the provided routine after the compilation has finished: ["Symbol n_b found in subroutine: my_algorithm.B, which is not among top level params: {'z', 'n'}."]
warnings.warn(
Now let's check the same fields of our compiled_routine object:
print("T gates for A:", compiled_routine.children["A"].resources["T_gates"].value)
print("Output size:", compiled_routine.ports["out"].size)
print("Total T gates:", compiled_routine.resources["T_gates"].value)
T gates for A: 2*n + z Output size: 2*n + z Total T gates: 2*n*z*ceiling(log2(2*n)) + 2*n + z
Since the resources in the children have type additive, bartiq automatically added the T_gates resource to the parent as a sum of the resources of the children.
Evaluation¶
Now it would be good to know what is the cost when we subsitute some numbers. We can do this using evaluate method. As you can see in the example below, it can either take all the parameters or just some of them.
from bartiq import evaluate
print("Different values of n:")
for n in range(6, 16, 2):
assignments = [f"n={n}"]
evaluated_routine = evaluate(compiled_routine, assignments)
print(f"n = {n}, total T gates:", evaluated_routine.resources["T_gates"].value)
z = 5
assignments = [f"n={n}", f"z={z}"]
evaluated_routine = evaluate(compiled_routine, assignments)
print(f"For n={n}, z={z}")
print("Total T gates:", evaluated_routine.resources["T_gates"].value)
Different values of n: n = 6, total T gates: 49*z + 12 n = 8, total T gates: 65*z + 16 n = 10, total T gates: 101*z + 20 n = 12, total T gates: 121*z + 24 n = 14, total T gates: 141*z + 28 For n=14, z=5 Total T gates: 733
Summary¶
Let's sum up what we covered in this tutorial:
- How to construct a simple algorithm in
bartiq - How to compile an estimate
- How to evaluate an estimate
- How to use the
qrefvisualization tool to visualize an algorithm
In the next tutorial we'll cover how to implement a more complex algorithm from a paper.