Advanced programming using pyAQASM
PyAQASM offers advanced functionalities that allow for very efficient synthesis of large circuits.
AbstractGates, circuit and matrix implementation
The AbstractGate
class allows you to define any parameterized gate you can think of. This can be quite convenient to describe, for instance, hardware dependent gate set
or to wrap quantum routines inside a “black box”.
However, when the time comes to simulate or properly execute the quantum circuits, one might need to provide additional information to these black boxes in order to either:
be able to simulate it (e.g. can we attach a matrix definition to the gate?) or
be able to link it using a sub-circuit library (e.g. can we attach a sub-circuit implementation to the gate?)
Matrix definition
To provide a matrix implementation for an AbstractGate
, one first needs to define a Python function that returns a matrix such that:
the function returns a matrix (for simplicity a numpy array)
the function has the same input signature as the gate
import numpy as np
from qat.lang.AQASM import AbstractGate
def phase_matrix(theta):
return np.diag([1, np.exp(1j * theta)])
phase_gate = AbstractGate("PHASE", [float], matrix_generator=phase_matrix, arity=1)
# Or alternatively
phase_gate = AbstractGate("PHASE", [float], arity=1)
phase_gate.set_matrix_generator(phase_matrix)
Subcircuit definition
Similarly, to provide a subcircuit implementation, one attaches to the definition of the gate a function returning a QRoutine
.
from qat.lang.AQASM import QRoutine, CNOT, PH, AbstractGate
def c_phase(theta):
routine = QRoutine()
wires = routine.new_wires(2)
CNOT(wires)
PH(-theta/2)(wires[1])
CNOT(wires)
PH(theta/2)(wires[0])
PH(theta/2)(wires[1])
return routine
c_phase_gate = AbstractGate("C_PHASE", [float], arity=2, circuit_generator=c_phase)
# Or alternatively
c_phase_gate = AbstractGate("C_PHASE", [float], arity=2)
c_phase_gate.set_circuit_generator(c_phase)
Arity generator
Finally, it is possible for abstract gates to have a variable arity that depends on its parameters.
from qat.lang.AQASM import AbstractGate
# A QFT has arity equals to its sole parameter
qft = AbstractGate("QFT", [int], arity=lambda n : n)
This way, pyAQASM is able to statically check that the gate is applied on the right number of qubits.
Lifting Python functions into quantum gates
Even though the above mechanics are very expressive when designing a library of quantum routines, it might still seem a bit clunky to have to define circuit generators and abstract gates separately.
The build_gate decorator removes a lot of this clunkyness by allowing you to turn any Python function returning a QRoutine
into an AbstractGate
.
from qat.lang.AQASM import QRoutine, H, build_gate, Program
@build_gate("WALSH_HADAMARD", [int], arity=lambda n: n)
def wht(nbqbits):
routine = QRoutine()
wires = routine.new_wires(nbqbits)
for wire in wires:
H(wire)
return routine
prog = Program()
qbits = prog.qalloc(4)
wht(4)(qbits)
# This circuit will contain a gate containing a subcircuit
# of length 4
circuit = prog.to_circ()
Linking at circuit extraction
In pyAQASM, it is possible to construct a Program
object using AbstractGate
objects representing subroutines, without specifying any implementation of the underlying subcircuit.
Consider for instance the following program:
from qat.lang.AQASM import Program, AbstractGate
prog = Program()
qbits = prog.qalloc(2 * 10)
adder = AbstractGate("ADD", [int, int], arity=lambda n1, n2: n1 + n2)
adder(10, 10)(qbits)
This code will run without error. The generated circuit (the result of to_circ()
) will contain a single gate without any subcircuit implementation. Therefore, attempting to execute this circuit on most QPUs will fail.
Now, let’s imagine that we have our very own implementation of an adder, lying in some Python namespace foo. Its definition will look like this:
from qat.lang.AQASM import QRoutine, H, build_gate
@build_gate("ADD", [int, int], arity=lambda n1, n2: n1 + n2)
def my_adder(length1, length2):
routine = QRoutine()
wires = routine.new_wires(length1 + length2)
## Here a proper addition is implemented
return routine
What pyAQASM allows us to do is to use this definition of an adder and link it to the program above in order to attach a proper subcircuit implementation to the ADD gate.
The linking is done inside the to_circ()
method of the Program
via the link keyword.
Lets update the first piece of code to link the implementation foo.my_adder to the ADD gate.
# This part stays the same
from qat.lang.AQASM import Program, AbstractGate
prog = Program()
qbits = prog.qalloc(2 * 10)
adder = AbstractGate("ADD", [int, int], arity=lambda n1, n2: n1 + n2)
adder(10, 10)(qbits)
import foo
circuit = prog.to_circ(link=[foo.my_adder])
Now the circuit variable contains a circuit that will probably be executable (depending on your implementation of my_adder). Equivalently one could have linked the full namespace foo, if we had for instance, many definitions of subcircuits inside it:
# This part stays the same
from qat.lang.AQASM import Program, AbstractGate
prog = Program()
qbits = prog.qalloc(2 * 10)
adder = AbstractGate("ADD", [int, int], arity=lambda n1, n2: n1 + n2)
adder(10, 10)(qbits)
import foo
circuit = prog.to_circ(link=[foo])
Compute/uncompute scopes
A quite common programming scheme in reversible computation in general, and quantum computation in particular, is the compute/uncompute scheme.
Usually, one has to compute some function, use the result of this computation, and then uncompute the first part of the circuit to free up some ancilla resources.
This scheme is natively supported inside the QRoutine
and Program
objects using a with statement:
from qat.lang.AQASM import QRoutine, CNOT, PH
routine = QRoutine()
# Allocating 2 wires to apply gates on
wires = routine.new_wires(2)
# Opening a computation scope
with routine.compute():
CNOT(wires[0], wires[1])
# The scope is now closed and stored internally
PH(1.)(wires[1])
# Here we 'pop' the last closed scope and apply its dagger
routine.uncompute()
# The routine now contains 3 gates
or, using programs:
from qat.lang.AQASM import Program, CNOT, PH
prog = Program()
# Allocating 2 wires to apply gates on
wires = prog.qalloc(2)
# Opening a computation scope
with prog.compute():
CNOT(wires[0], wires[1])
# The scope is now closed and stored internally
PH(1.)(wires[1])
# Here we 'pop' the last closed scope and apply its dagger
prog.uncompute()
# The routine now contains 3 gates
Of course computation scopes can be nested:
from qat.lang.AQASM import QRoutine, CNOT, PH
routine = QRoutine()
wires = routine.new_wires(3)
with routine.compute():
CNOT(wires[0], wires[1])
with routine.compute():
CNOT(wires[1], wires[2])
PH(1.)(wires[2])
routine.uncompute()
PH(2.)(wires[1])
routine.uncompute()
# The routine now contains 9 gates
Automated ancillae management
The QRoutine
object comes with a system of ancillae management.
In practice, one can allocate fresh wires and declare them as ancillae:
from qat.lang.AQASM import QRoutine
routine = QRoutine()
wires = routine.new_wires(1)
ancilla = routine.new_wires(1)
routine.set_ancillae(ancilla)
This routine will have arity 1, its second wire being declared as an ancilla.
Upon calling the to_circ()
method of a program containing this routine, additional qubits will
be dynamically allocated and passed to the routine. Of course this allocation is made recursively accros
the call tree.
The only thing you have to ensure is that ancillae are freed before leaving the routine (i.e are in product \(|0\rangle\) state).
As a consequence, it is possible to link subcircuit implementations of a gate using different number of ancillae:
from qat.lang.AQASM import Program
from qat.lang.AQASM.arithmetic import add
import qat.lang.AQASM.qftarith as qftarith
import qat.lang.AQASM.classarith as classarith
prog = Program()
qbits = prog.qalloc(20)
add(10, 10)(qbits)
# No ancillae
circuit = prog.to_circ(link=[qftarith])
# 9 ancillae
circuit = prog.to_circ(link=[classarith])
Oracles and quantum types
When writing an oracle based quantum algorithm, such as Grover’s aglorithm, it is sometimes hard to translate a classical function implementing the oracle into a proper quantum circuit. Usually, this translation requires management of temporary resources and intermediate computations that can quickly become overwhelming (and, to be fair, not necessarily interesting).
PyAQASM comes with a nice feature that can help you efficiently describe complicated quantum oracles: high(er)-level quantum types.
It is possible, for instance, to declare a quantum register as a QInt
. Quantum integers can then be used to directly perform arithmetic operations, or comparisons.
For instance, the following piece of code allocates two quantum integers and adds them:
from qat.lang.AQASM import Program
from qat.lang.AQASM.qint import QInt
prog = Program()
qint1 = prog.qalloc(5, QInt)
qint2 = prog.qalloc(5, QInt)
qint1 += qint2
# Abstract circuit with some unimplemented adder
circuit_abs = prog.to_circ()
import qat.lang.AQASM.qftarith as qftarith
import qat.lang.AQASM.classarith as classarith
# Using QFT based adder
circuit_qft = prog.to_circ(link=[qftarith])
# Using carry based adder
circuit_class = prog.to_circ(link=[classarith])
In the following subsections, we detail the two quantum types implemented in pyAQASM: QBool
and QInt
.
Quantum booleans, quantum conditionals, and quantum oracles
The simplest quantum type is the QBool
type (or QBoolArray
for registers).
Allocation
Allocation can be done, as for any other type, by adding the corresponding type to the qalloc()
or new_wires()
method. Since registers are arrays of qubits, they can only be typed using the QBoolArray
class.
from qat.lang.AQASM import Program, QRoutine
from qat.lang.AQASM.qbool import QBoolArray
prog = Program()
qbools = prog.qalloc(3, QBoolArray)
print(type(qbools))
print(type(qbools[0]))
# or
rout = QRoutine()
qbools = rout.new_wires(3, QBoolArray)
print(type(qbools))
print(type(qbools[0]))
<class 'qat.lang.AQASM.qbool.QBoolArray'>
<class 'qat.lang.AQASM.qbool.QBool'>
<class 'qat.lang.AQASM.qbool.QBoolArray'>
<class 'qat.lang.AQASM.qbool.QBool'>
Logical expressions
QBool
can be composed using Python’s logical operators (and, or, not, xor) to form boolean expressions:
from qat.lang.AQASM import QRoutine
from qat.lang.AQASM.qbool import QBoolArray
rout = QRoutine()
qbools = rout.new_wires(3, QBoolArray)
and_expr = qbools[0] & qbools[1] & qbools[2]
print(and_expr)
expr1 = qbools[0] & qbools[1] | qbools[2]
print(expr1)
expr2 = qbools[0] ^ qbools[1] | ~qbools[2]
print(expr2)
(q[0]&q[1]&q[2])
((q[0]&q[1])|q[2])
((q[0]^q[1])|(~ q[2]))
Evaluating expressions
How good is it to be able to construct expressions if we can’t use them?
Boolean expressions can be evaluated. Evaluating an expression will append a sequence of gates to the current scope (i.e the Program
or the QRoutine
in which the QBool
were declared). This sequence of gates will evaluate the expression and output the result in a temporary qubit.
For instance the following code:
from qat.lang.AQASM import QRoutine
from qat.lang.AQASM.qbool import QBoolArray
rout = QRoutine()
qbools = rout.new_wires(3, QBoolArray)
expr2 = qbools[0] ^ qbools[1] | ~qbools[2]
output = expr2.evaluate()
print(rout.arity)
3
will produce the following circuit:
In this circuit, we have:
two CNOTs to compute the XOR of the first two qubits in some ancilla \(q_4\), (corresponding to the qbools[0] ^ qbools[1] term)
a CNOT and a X gate to compute the ~qbools[2] term in another ancilla \(q_5\)
5 X gates and a Toffoli gate to compute the final OR operator in \(q_3\), the output qubit. Here we use the de Morgan law to turn our OR into NOTS and a AND operator.
Note
Notice that, in this example, the produced routine has arity 3. Indeed, the qubit used in the .evaluate method are automatically flagged as ancillae. Keep that in mind when using the .evaluate method.
It is also possible to specify an output qubit for the evaluation procedure:
from qat.lang.AQASM import QRoutine
from qat.lang.AQASM.qbool import QBoolArray
rout = QRoutine()
qbools = rout.new_wires(3, QBoolArray)
output = rout.new_wires(1)
expr2 = qbools[0] ^ qbools[1] | ~qbools[2]
expr2.evaluate(output=output)
print(rout.arity)
4
will produce the same circuit, but this time the routine will have arity 4 since output was declared as a proper input of the routine.
Quantum conditionals and with statements
Expressions can also be combined with a with statement to produce a context in which the expression is evaluated. For instance, the following piece of code evaluates an expression in a with statement and copies the result of the evaluation in an output qubit.
from qat.lang.AQASM import QRoutine, CNOT
from qat.lang.AQASM.qbool import QBoolArray
rout = QRoutine()
qbools = rout.new_wires(3, QBoolArray)
output = rout.new_wires(1)
expr2 = qbools[0] ^ qbools[1] | ~qbools[2]
with expr2 as condition:
CNOT(condition, output)
print(output[0], condition[0])
q[3] q[4]
This will produce the following circuit:
Notice that the boolean expression was first evaluated in a temporary qubit (qubit 4), then we copied the result in an output qubit (qubit 3), finally, the expression was uncomputed upon exiting the with block.
The with statement is in fact just a syntactic sugar for the following code:
from qat.lang.AQASM import QRoutine, CNOT
from qat.lang.AQASM.qbool import QBoolArray
rout = QRoutine()
qbools = rout.new_wires(3, QBoolArray)
output = rout.new_wires(1)
expr2 = qbools[0] ^ qbools[1] | ~qbools[2]
# starting a compute scope
with rout.compute():
# evaluating the expression
condition = expr2.evaluate()
# copying the result
CNOT(condition, output)
# uncomputing the evaluation
rout.uncompute()
# releasing the ancilla `condition` for later use
rout.free_ancillae(condition)
Warning
Quantum conditionals cannot be called on an expression whose underlying qubits were allocated in a Program class.
Therefore, it should only be used inside a QRoutine
first.
Building phase oracles
Last but not least, it is possible to use a boolean expression to automatically generate a quantum circuit that will flip the phase of basis states that verify the expression. This is done by calling the method phase on an expression.
from qat.lang.AQASM import Program, CNOT, H
from qat.lang.AQASM.qbool import QBoolArray
prog = Program()
qbools = prog.qalloc(2, QBoolArray)
for qbit in qbools:
H(qbit)
expr = qbools[0] & qbools[1]
expr.phase()
job = prog.to_circ().to_job()
from qat.qpus import get_default_qpu
result = get_default_qpu().submit(job)
for sample in result:
print(sample.state, sample.amplitude)
|[False, False]> (0.49999999999999994+0j)
|[False, True]> (0.49999999999999994+0j)
|[True, False]> (0.49999999999999994+0j)
|[True, True]> (-0.49999999999999994+0j)
Since our expression evaluates at True if and only if the two qubits are set to True only state \(|11\rangle\) had its phase flipped.
Warning
The phase method cannot be called on an expression whose underlying qubits were allocated in a Program class.
Therefore, it should only be used inside a QRoutine
first.
Quantum integers
The second type allows to type registers as integers.
Allocation
Similarly to QBool
, QInt
is allocated using the qalloc()
or new_wires()
methods.
from qat.lang.AQASM import Program, QRoutine
from qat.lang.AQASM.qint import QInt
prog = Program()
qint = prog.qalloc(5, QInt)
print(qint)
rout = QRoutine()
qint = rout.new_wires(5, QInt)
print(qint)
QInt(q[0]..q[4])
QInt(q[0]..q[4])
Setting a classical value
QInt
have a method called set_value()
that xors the content of the QInt
with a classical value.
from qat.lang.AQASM import QRoutine
from qat.lang.AQASM.qint import QInt
rout = QRoutine()
qint = rout.new_wires(2, QInt)
qint.set_value(3)
produces:
Arithmetic expressions
Instances of QInt
can be combined via addition and multiplication. Combining them forms an arithmetic expression without adding any gate to the underlying circuit.
The only operation that triggers a circuit generation is the += operator.
from qat.lang.AQASM import Program, QRoutine
from qat.lang.AQASM.qint import QInt
prog = Program()
qint1 = prog.qalloc(5, QInt)
qint2 = prog.qalloc(5, QInt)
qint1 + qint2
circuit1 = prog.to_circ()
qint1 += qint2
circuit2 = prog.to_circ()
print("First circuit")
for op in circuit1.iterate_simple():
print(op)
print("Second circuit")
for op in circuit2.iterate_simple():
print(op)
First circuit
Second circuit
('ADD', [5, 5], [4, 3, 2, 1, 0, 9, 8, 7, 6, 5])
Notice that the first circuit is empty: the statement qint1 + qint2 did not produce any gate. The second statement, qint1 += qint2 did produce an adder.
The same holds for multiplication:
from qat.lang.AQASM import Program, QRoutine
from qat.lang.AQASM.qint import QInt
prog = Program()
qint1 = prog.qalloc(5, QInt)
qint2 = prog.qalloc(5, QInt)
qint3 = prog.qalloc(5, QInt)
qint1 * qint2
circuit1 = prog.to_circ()
qint3 += qint1 * qint2
circuit2 = prog.to_circ()
print("First circuit")
for op in circuit1.iterate_simple():
print(op)
print("Second circuit")
for op in circuit2.iterate_simple(depth=0):
print(op)
First circuit
Second circuit
('MULT', [5, 5, 5], [4, 3, 2, 1, 0, 9, 8, 7, 6, 5, 14, 13, 12, 11, 10])
Conditionals on quantum integers
It is possible to compare quantum integers with one another or to some classical values. These comparisons produce QBool
objects and can thus be used to produce oracles or conditional statements.
For instance, one can write a program that:
compares two quantum integers
and increments a quantum integer if and only if the comparison fails
from qat.lang.AQASM import QRoutine
from qat.lang.AQASM.qint import QInt
rout = QRoutine()
qint1 = rout.new_wires(5, QInt)
qint2 = rout.new_wires(5, QInt)
qint3 = rout.new_wires(5, QInt)
with qint1 != qint2 as condition:
qint3 += condition.cast_to(QInt)
Notice the usage of the cast_to method that allows us to cast any type to a new type (by re-wrapping the quantum register).
Of course, one can also use the .evaluate and .phase methods. For instance, the following piece of code flips the phase of all classical states that represent integers below some constant, say 5:
from qat.lang.AQASM import Program, QRoutine, H
from qat.lang.AQASM.qint import QInt
import qat.lang.AQASM.qftarith as qftarith
rout = QRoutine()
qint = rout.new_wires(3, QInt)
for qbit in qint:
H(qbit)
(qint < 5).phase()
prog = Program()
qbits = prog.qalloc(3, QInt)
rout(qbits)
circuit = prog.to_circ(link=[qftarith])
job = circuit.to_job()
from qat.qpus import get_default_qpu
result = get_default_qpu().submit(job)
for sample in result:
print(sample.state, sample.amplitude)
|0>|Anc:0> (-0.35355339059327334+9.323307507451124e-17j)
|1>|Anc:0> (-0.3535533905932733+5.034254676965732e-17j)
|2>|Anc:0> (-0.3535533905932734+1.0884348908992939e-16j)
|3>|Anc:0> (-0.3535533905932733+7.54030915644243e-17j)
|4>|Anc:0> (-0.3535533905932735+5.703410071228764e-17j)
|5>|Anc:0> (0.353553390593273-1.2696513998078917e-16j)
|6>|Anc:0> (0.35355339059327306-1.0509864824017613e-16j)
|7>|Anc:0> (0.3535533905932732-1.6183876264232977e-16j)
Example: Grover oracle for graph coloring
Using these tools, it is quite straightforward to write simple phase oracles for Grover-like applications.
For instance, one can write in a few lines of code a routine that will act as an oracle for a graph coloring problem.
We will construct a Python function that will take a graph (networkx.Graph) and a number of bits to use to store each color. It will return a QRoutine
that flips the phase of a basis state if and only if it describes a clean coloration.
from functools import reduce
from qat.lang.AQASM import QRoutine
from qat.lang.AQASM.qint import QInt
def coloring_oracle(graph, bitlength):
rout = QRoutine()
colors = [rout.new_wires(bitlength, QInt) for _ in graph.nodes()]
reduce(
lambda a, b: a & b,
(colors[a] != colors[b] for a, b in graph.edges())
).phase()
return rout
import networkx as nx
graph = nx.generators.path_graph(20)
bitlength = 1 # looking for 2 colorings, so colors are stored on a single bit
oracle = coloring_oracle(graph, bitlength)
print("Oracle has arity:", oracle.arity)
print("Oracle uses {} ancillae".format(len(oracle.ancillae)))
Oracle has arity: 20
Oracle uses 19 ancillae
In this function, we:
declare an array of quantum integers, one for each vertex of our graph
build a formula that computeс the logical and of \(c_i \neq c_j\) for each edge \(i\) \(j\) of our graph
use this formula to perform a phase flip
This implementation uses a lot of qubits. For each \(c_i \neq c_j\) in our for loop, pyAQASM allocates a temporary qubit. Then a generalized controlled \(Z\) is used to flip the phase of the states that have all these ancillae set to 1. Finally, all the temporary qubits are freed by uncomputing the \(c_i \neq c_j\) statements.
Asymptotically, this oracle uses \(b|V| + |E|\) qubits, without considering the fact that the implementation of the generalized controlled \(Z\) might require additional qubits.
It is possible to save up some qubits, at the cost of an increased number of gates. The following code snippet implements the same oracle, but a bit differently:
it allocates a counter large enough to count up to \(|E|\)
for each edge, if \(c_i \neq c_j\), it increments the counter by 1
it flips the phase of the basis state if and only if the counter contains exactly \(|E|\)
finally, it uncomputes all the counter increments
from qat.lang.AQASM import QRoutine
from qat.lang.AQASM.qint import QInt
def coloring_oracle(graph, bitlength):
rout = QRoutine()
colors = [rout.new_wires(bitlength, QInt) for _ in graph.nodes()]
counter = rout.new_wires(graph.number_of_edges().bit_length(), QInt)
with rout.compute():
for a, b in graph.edges():
with colors[a] != colors[b] as condition:
counter += condition.cast_to(QInt)
(counter == graph.number_of_edges()).phase()
rout.uncompute()
rout.set_ancillae(counter)
return rout
import networkx as nx
graph = nx.generators.path_graph(20)
bitlength = 1 # looking for 2 colorings, so colors are stored on a single bit
oracle = coloring_oracle(graph, bitlength)
print("Oracle has arity:", oracle.arity)
print("Oracle uses {} ancillae".format(len(oracle.ancillae)))
Oracle has arity: 20
Oracle uses 6 ancillae
Asymptotically, this oracle is far more frugal since it uses only \(b|V| + log(|E|)\) qubits.
Notice that in both examples, we didn’t have to directly mention any quantum gate. Everything is handled by the quantum types.