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 a circuit_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.

In [1]:
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:

In [2]:
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()
No description has been provided for this image
No description has been provided for this image

It is easy to see that both circuit implement the same instruction flow by simply iterating over the circuit itself:

In [3]:
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:

In [4]:
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)
No description has been provided for this image

We can check that the two circuits are in fact identical (i.e produce the exact same instruction flow):

In [5]:
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. 
In [ ]: