Circuit inlining¶
Structure of the Circuit object¶
Circuits generated by pyAQASM have a particular structure that allows to store efficiently the instruction flow resulting from its execution.
Circuits are composed of a main body: the .ops
field. It consists in a simple list of operations. Operations can be any of basic instructions supported by (py)AQASM (i.e measure, reset, quantum gate, etc...).
Quantum gates used inside this main body (or anywhere else inside the circuit) are references to an entry of a gate dictionary stored in the .gateDic
field of the circuit.
In order to be space and informationally efficient, a gate can be defined in several different fashions (not exclusive):
- via a matrix : this is the case of all the basic gates
- via some syntax (i.e name + list of parameters): this is also the case for all the basic gates, but also any
AbstractGate
- via some circuit implementation: this is the case for any
AbstractGate
that has acircuit_generator
The former being the equivalent of a function call: a gate can be seen as a function applying a fixed circuit (or stream of instructions) on a list of arguments, the arguments being the qubits.
In particular, storing each recurrent subcircuit inside an AbstractGate
will factorise a large portion of the circuit into a single gate definition.
Inlining¶
When generating a circuit using pyAQASM, you can ask the to_circ
method to go down into each gate definition and inline the circuit implementation of the gate inside the main body of the circuit. This process is costly, and is deactivated by default (argument inline
, see the documentation of to_circ
).
Almost all the simulators in the QLM (and myQLM) support the emulation of the frame stack during the simulation.
In order to unify the way the stack is managed, the Circuit
structure itself has a built-in iterator emulating the execution flow generated by this stack emulation.
from qat.lang.AQASM import *
from qat.lang.AQASM.misc import build_gate
from qat.lang.AQASM.qftarith import QFT
@build_gate("FOO", [int], arity=lambda k: k)
def foo(k):
rout = QRoutine()
wires = rout.new_wires(k)
for w in wires:
rout.apply(X, w)
return rout
@build_gate("BAR", [int, int], arity=lambda n, k: n + k)
def bar(n, k):
rout = QRoutine()
reg1 = rout.new_wires(n)
reg2 = rout.new_wires(k)
rout.apply(QFT(n), reg1)
rout.apply(foo(k), reg2)
return rout
Now foo
and bar
are two abstract gates with circuit implementation.
Notice that bar
also calls a QFT
which is also an abstract gate with circuit implementation.
One can generate the full circuit using the inline
keyword in the to_circ
method:
prog = Program()
qbits = prog.qalloc(5 + 3)
prog.apply(bar(5, 3), qbits)
circuit_default = prog.to_circ()
circuit_inlined = prog.to_circ(inline=True)
circuit_default.display()
circuit_inlined.display()
It is easy to see that both circuit implement the same instruction flow by simply iterating over the circuit itself:
for ope in circuit_default:
print(ope)
Op(gate='H', qbits=[0], type=0, cbits=None, formula=None, remap=None) Op(gate='_4', qbits=[1, 0], type=0, cbits=None, formula=None, remap=None) Op(gate='_6', qbits=[2, 0], type=0, cbits=None, formula=None, remap=None) Op(gate='_8', qbits=[3, 0], type=0, cbits=None, formula=None, remap=None) Op(gate='_10', qbits=[4, 0], type=0, cbits=None, formula=None, remap=None) Op(gate='H', qbits=[1], type=0, cbits=None, formula=None, remap=None) Op(gate='_4', qbits=[2, 1], type=0, cbits=None, formula=None, remap=None) Op(gate='_6', qbits=[3, 1], type=0, cbits=None, formula=None, remap=None) Op(gate='_8', qbits=[4, 1], type=0, cbits=None, formula=None, remap=None) Op(gate='H', qbits=[2], type=0, cbits=None, formula=None, remap=None) Op(gate='_4', qbits=[3, 2], type=0, cbits=None, formula=None, remap=None) Op(gate='_6', qbits=[4, 2], type=0, cbits=None, formula=None, remap=None) Op(gate='H', qbits=[3], type=0, cbits=None, formula=None, remap=None) Op(gate='_4', qbits=[4, 3], type=0, cbits=None, formula=None, remap=None) Op(gate='H', qbits=[4], type=0, cbits=None, formula=None, remap=None) Op(gate='X', qbits=[5], type=0, cbits=None, formula=None, remap=None) Op(gate='X', qbits=[6], type=0, cbits=None, formula=None, remap=None) Op(gate='X', qbits=[7], type=0, cbits=None, formula=None, remap=None)
Notice that iterating over the circuit or interating over the .ops
field of the circuit is not the same thing!
The .ops
field will only contain the main of the circuit.
In our example, circuit_default's main contains a single gate:
for ope in circuit_default.ops:
print(ope)
circuit_default.display()
Op(gate='_0', qbits=[0, 1, 2, 3, 4, 5, 6, 7], type=0, cbits=None, formula=None, remap=None)
We can check that the two circuits are in fact identical (i.e produce the exact same instruction flow):
for op1, op2 in zip(circuit_default, circuit_inlined):
assert op1 == op2
In most cases, it is more efficient to leave the `inline` option to False.