Basic example¶
In this tutorial our goals are to learn:
- How to use
bartiq
to implement a quantum algorithm from a paper - How to obtain resource estimates for that algorithm
- What the most important concepts used in
bartiq
are
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 toN
and 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.
routine_a = {
"name": "a",
"type": None,
"ports": [
{"name": "in", "direction": "input", "size": "N_a"},
{"name": "out", "direction": "output", "size": "N_a"},
],
}
routine_b = {
"name": "b",
"type": None,
"ports": [
{"name": "in", "direction": "input", "size": "N_b"},
{"name": "out", "direction": "output", "size": "N_b"},
],
}
We will need to know how much each subroutine costs if we want to run the resource estimation.
In fault-tolerant algorithm the resource that people usually care about is how many T gates do you need to implement an algorithm. Therefore, let's say that a
costs 2*N_a
T gates and b
costs N_b*ceil(log_2(N_b))
T gates. This will require adding two new fields to our dictionary:
routine_a["input_params"] = ["N_a"]
routine_b["input_params"] = ["N_b"]
routine_a["resources"] = [{"name": "T_gates", "type": "additive", "value": "2*N_a"}]
routine_b["resources"] = [{"name": "T_gates", "type": "additive", "value": "N_b*ceil(log_2(N_b))"}]
As you can see we added two new fields to our dictionaries:
input_params
: define what variables are used to define resources and register sizes.resources
: what resources are needed for a particular subroutine. As you can see resources have the following fields:name
: name of the resourcetype
:bartiq
allows for the following types:additive
,multiplicative
,qubits
andother
.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["linked_params"] = [{"source": "N", "targets": ["a.N_a", "b.N_b"]}]
The new things we have here are:
connections
: how ports are connected. Each connection has source and target.children
: specifies what subroutines does a particular routine consist of.linked_params
: tells us how the parameters of the parent are linked to the parameters of children. In this case, it tells us that parameterN
from this routine translates toN_a
in childa
andN_b
in childb
.
The last step is just a formality to indicate which version of QREF schema we use:
my_algorithm_qref = {'version': 'v1', 'program': my_algorithm}
Now we can translate our algorithm into a proper bartiq
routine and see what's the total cost of my_algorithm
.
NOTE:
- You might be wondering, why
my_algorithm
didn't needinput_params
? That's because itN
does not show up in any expressions – we don't have resources defined here. It does show up in the ports though, so we still need to add it tolinked_params
. - Unfortunately at the moment register sizes need to be single variables – fortunately, as we'll see later, there's a way around it using
local_variables
.
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">}
The cost of "a" is still expressed in terms of its own "local" variable, N_a
. 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(name='out', parent=<Routine name="my_algorithm">, direction='output', size=None, meta={})
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 (we highlighted in green what changed from the previous one):
We do this with the following command:
from bartiq import compile_routine
compiled_routine = compile_routine(uncompiled_routine)
Now if we check the same fields of our compiled_routine
object, we'll see that everything is expressed in terms of the
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 Output size: N Total T gates: N*ceiling(log2(N)) + 2*N
Evaluation¶
Now it would be good to know what is the cost when we subsitute some numbers. We can do this using evaluate
method:
from bartiq import evaluate
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)
N = 6, total T gates: 30 N = 8, total T gates: 40 N = 10, total T gates: 60 N = 12, total T gates: 72 N = 14, total T gates: 84
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
In the next tutorial we'll cover how to implement a more complex algorithm from a paper.