2.10. Detailed Sequencing Walkthrough

This example will provide two step-by-step illustrations of the internals of the sequencing process. Note that this will involve some calls into the object structure to unveil internals which are not intended to be made in a productive use case and produce some very technical outputs. These are broken down and explained in detail where necessary.

2.10.1. Example 1 (Software-Evaluated Loop Condition)

In the first example, we will emulate the behaviour of a RepetitonPulseTemplate to repeats a TablePulseTemplate for a fixed number of times using a LoopPulseTemplate with a SoftwareCondition. We have done so already in the example for conditional execution but here we will explore the sequencing process in detail. The definitions of the classes are the following (resulting in 2 repetitions):

In [1]:
from qctoolkit.pulses import LoopPulseTemplate, TablePulseTemplate, SoftwareCondition, Sequencer

# define a table pulse template which we want to repeat (including a parameter)
table_template = TablePulseTemplate()
table_template.add_entry(1, 'foo', 'linear')
table_template.add_entry(3, 'foo')
table_template.add_entry(4, 0, 'linear')

# define a software condition will evaluate to true as long as the loop counter is less than 5 and false afterwards
repeat_condition = SoftwareCondition(lambda x: x < 2) # it will never require an interruption of the sequencing process

# define a loop template consisting of the table template as body and a condition identified by 'rcon'
loop_template = LoopPulseTemplate('rcon', table_template)

# provide sequencing mappings: condition 'rcon' -> repeat_condition and parameter 'foo' -> 2
conditions = {'rcon': repeat_condition}
parameters = {'foo': 2}

# create a Sequencer instance and push our loop template on the sequencing stack with the corresponding mappings
s = Sequencer()
s.push(loop_template, parameters, conditions)

# store references to the main instruction block and the corresponding sequencing stack
main_block = s._Sequencer__main_block
sequencing_stack = s._Sequencer__sequencing_stacks[main_block]
In [2]:
print(sequencing_stack) # print the current sequencing stack for the main block
[(<qctoolkit.pulses.loop_pulse_template.LoopPulseTemplate object at 0x0000000007395438>, {'foo': <ConstantParameter 2>}, {'rcon': <qctoolkit.pulses.conditions.SoftwareCondition object at 0x0000000002A42550>})]

As you can see in the dump of the sequencing stack of the main instruction block, there is currently one item on the stack, which a tuple consisting of our LoopPulseTemplate loop_template and the mappings parameters and conditions. The following figure illustrates the current content sequencing stack.

The sequencing stack after pushing ``loop_template``

The sequencing stack after pushing loop_template

Running Sequencer.build() would run the entire sequencing process, resulting in the desired instruction sequence. However, since we want to understand the process itself, we will perform the necessary steps ourselves by manually calling the corresponding functions. We now translate the topmost (and only) stack item:

In [3]:
# get the topmost item from the sequencing stack
(template, params, conds) = sequencing_stack[-1]
# remove template from stack and translate it it does not require a stop
if not template.requires_stop(params, conds):
    sequencing_stack.pop()
    template.build_sequence(s, params, conds, main_block)

The build_sequence method looks up the condition identified by ‘rcon’ in the conditions map conditions which is our repeat_condition object define dabove. It then invokes the build_sequence_loop method of this object. Being a SoftwareCondition object, it evaluates its evaluation function, which returns true, and thus adds the body, our table_template to the sequencing stack. Since the loop condition must be evaluated again after the loop body was run, also the loop_template is pushed to the stack again. Thus, the stack now looks as follows:

In [4]:
print(sequencing_stack) # print the current sequencing stack for the main block
[(<qctoolkit.pulses.loop_pulse_template.LoopPulseTemplate object at 0x0000000007395438>, {'foo': <ConstantParameter 2>}, {'rcon': <qctoolkit.pulses.conditions.SoftwareCondition object at 0x0000000002A42550>}), (<qctoolkit.pulses.table_pulse_template.TablePulseTemplate object at 0x0000000004664BE0>, {'foo': <ConstantParameter 2>}, {'rcon': <qctoolkit.pulses.conditions.SoftwareCondition object at 0x0000000002A42550>})]
The sequencing stack after translating ``loop_template``

The sequencing stack after translating loop_template

Note that no waveforms or instructions have been generated so far, i.e., the main instruction block is empty:

In [5]:
print(main_block._InstructionBlock__instruction_list) # print all instructions in the main block
[]

Sequencer would now enter the next iteration, i.e., pop and translate the topmost element from the stack.

In [6]:
# get the topmost item from the sequencing stack
(template, params, conds) = sequencing_stack[-1]
# remove template from stack and translate it it does not require a stop
if not template.requires_stop(params, conds):
    sequencing_stack.pop()
    template.build_sequence(s, params, conds, main_block)

This time, our table_template, that is, the body of the loop, is at the top. It’s translation via build_sequence() looks up the parameter value for ‘foo’, generates a waveform and inserts a corresponding instruction in the main block:

In [7]:
print(main_block._InstructionBlock__instruction_list) # print all instructions in the main block
[<qctoolkit.pulses.instructions.EXECInstruction object at 0x0000000007395518>]

Since we’ve successfully processed the table_template item on the sequencing stack, we are left with a loop_template item. That means, the stack looks just like in the beginning (refer to Figure 1).

In [8]:
print(sequencing_stack)
[(<qctoolkit.pulses.loop_pulse_template.LoopPulseTemplate object at 0x0000000007395438>, {'foo': <ConstantParameter 2>}, {'rcon': <qctoolkit.pulses.conditions.SoftwareCondition object at 0x0000000002A42550>})]

We will fetch it from the stack and translate it. Since the loop counter in the SoftwareCondition object is currently 1, it will still evaluate to true, meaning that the loop continues, i.e., the body template and the loop template are again pushed to the stack (cf. Figure 2).

In [9]:
# get the topmost item from the sequencing stack
(template, params, conds) = sequencing_stack[-1]
# remove template from stack and translate it it does not require a stop
if not template.requires_stop(params, conds):
    sequencing_stack.pop()
    template.build_sequence(s, params, conds, main_block)

print(sequencing_stack)
[(<qctoolkit.pulses.loop_pulse_template.LoopPulseTemplate object at 0x0000000007395438>, {'foo': <ConstantParameter 2>}, {'rcon': <qctoolkit.pulses.conditions.SoftwareCondition object at 0x0000000002A42550>}), (<qctoolkit.pulses.table_pulse_template.TablePulseTemplate object at 0x0000000004664BE0>, {'foo': <ConstantParameter 2>}, {'rcon': <qctoolkit.pulses.conditions.SoftwareCondition object at 0x0000000002A42550>})]

Proceeding as before, we translate the topmost element, which is again the loop body table_template. This results in the expected EXECInstruction and a stack in which the loop_template remains for reevaluation.

In [10]:
# get the topmost item from the sequencing stack
(template, params, conds) = sequencing_stack[-1]
# remove template from stack and translate it it does not require a stop
if not template.requires_stop(params, conds):
    sequencing_stack.pop()
    template.build_sequence(s, params, conds, main_block)

print(sequencing_stack)
[(<qctoolkit.pulses.loop_pulse_template.LoopPulseTemplate object at 0x0000000007395438>, {'foo': <ConstantParameter 2>}, {'rcon': <qctoolkit.pulses.conditions.SoftwareCondition object at 0x0000000002A42550>})]

Our main instruction block now contains two EXECInstructions:

In [11]:
print(main_block._InstructionBlock__instruction_list) # print all instructions in the main block
[<qctoolkit.pulses.instructions.EXECInstruction object at 0x0000000007395518>, <qctoolkit.pulses.instructions.EXECInstruction object at 0x00000000076BC908>]

We are left with the loop_template on the stack, which we will translate in the following. However, this time the repeat_condition will evaluate to false, meaning that neither body nor loop template are pushed to the stack. We are done with the loop.

In [12]:
# get the topmost item from the sequencing stack
(template, params, conds) = sequencing_stack[-1]
# remove template from stack and translate it it does not require a stop
if not template.requires_stop(params, conds):
    sequencing_stack.pop()
    template.build_sequence(s, params, conds, main_block)

print(sequencing_stack)
[]

Note that we have not yet obtained an InstructionSequence but only constructed the main InstructionBlock object. We will conclude the sequencing by converting the main block into the desired sequence:

In [13]:
instruction_sequence = main_block.compile_sequence()
print(instruction_sequence)
[<qctoolkit.pulses.instructions.EXECInstruction object at 0x0000000007395518>, <qctoolkit.pulses.instructions.EXECInstruction object at 0x00000000076BC908>, <qctoolkit.pulses.instructions.STOPInstruction object at 0x00000000076B6C88>]

In this case, this is a trivial task, as it simply takes both instructions contains in the main block and adds a STOPInstruction to generate the sequence. Now we are done.

In [14]:
print(s.has_finished()) # are we done?
True

We have explored what happens internally when we invoke Sequencer.build() on our loop_template. In a productive use case, we can let Sequencer handle all of this and get the same result (apart from memory addresses of the involved python objects):

In [15]:
s = Sequencer()
repeat_condition = SoftwareCondition(lambda x: x < 2) # it will never require an interruption of the sequencing process
conditions = {'rcon': repeat_condition}
s.push(loop_template, parameters, conditions)
instruction_sequence = s.build()
print(instruction_sequence)
[<qctoolkit.pulses.instructions.EXECInstruction object at 0x00000000076B6128>, <qctoolkit.pulses.instructions.EXECInstruction object at 0x00000000076B6278>, <qctoolkit.pulses.instructions.STOPInstruction object at 0x00000000076B6160>]

2.10.2. Example 2 (Hardware Evaluated Branch Nested In Loop)

In this example we want to look into hardware-based branching evaluation based using the HardwareCondition class and how InstructionBlocks are created and handled during the Sequencing process. The pulse we want to translate is a loop which contains a branch template (if-else-construct) which in turn contains table pulse templates:

In [16]:
from qctoolkit.pulses import LoopPulseTemplate, BranchPulseTemplate, TablePulseTemplate, HardwareCondition, Sequencer
from qctoolkit.pulses import Trigger

# two table pulse templates for the alternative paths of the branch pulse template
# they differ in their interpolation behaviour (jump vs linear ramp)
pos_template = TablePulseTemplate()
pos_template.add_entry(1, 'foo', 'linear')
pos_template.add_entry(3, 'foo')
pos_template.add_entry(4, 0, 'linear')

neg_template = TablePulseTemplate()
neg_template.add_entry(1, 'foo')
neg_template.add_entry(3, 'foo')
neg_template.add_entry(4, 0)

parameters = {'foo': 2}

# the branch pulse template
branch_template = BranchPulseTemplate('bcon', pos_template, neg_template)

# the loop pulse template
loop_template = LoopPulseTemplate('lcon', branch_template)


# for this example: Introduce a trigger that can be identified by a name
class NamedTrigger(Trigger):
    def __init__(self, name: str) -> None:
        self.name = name

    def __str__(self) -> str:
        return "Trigger '{}'".format(self.name)

# create HardwareCondition objects for branch and loop
branch_condition = HardwareCondition(NamedTrigger("branch_trigger"))
loop_condition   = HardwareCondition(NamedTrigger("loop_trigger"))

# mapping of identifiers to conditions
conditions = {'bcon': branch_condition, 'lcon': loop_condition}

# create a Sequencer instance and push our loop template on the sequencing stack with the corresponding mappings
s = Sequencer()
s.push(loop_template, parameters, conditions)

# store references to the main instruction block and the corresponding sequencing stack
main_block = s._Sequencer__main_block
main_sequencing_stack = s._Sequencer__sequencing_stacks[main_block]

The sequencing stack now contains a single entry, namely the tuple containing our ‘loop_template’ and the mappings ‘parameters’ and ‘conditions’:

In [17]:
print(main_sequencing_stack)
[(<qctoolkit.pulses.loop_pulse_template.LoopPulseTemplate object at 0x00000000076BCFD0>, {'foo': <ConstantParameter 2>}, {'bcon': <qctoolkit.pulses.conditions.HardwareCondition object at 0x00000000076B11D0>, 'lcon': <qctoolkit.pulses.conditions.HardwareCondition object at 0x00000000076B1160>})]
The initial sequencing stack for example 2

The initial sequencing stack for example 2

Entering the sequencing process, we translate the topmost element as before:

In [18]:
# get the topmost item from the sequencing stack
(template, params, conds) = main_sequencing_stack[-1]
# remove template from stack and translate it it does not require a stop
if not template.requires_stop(params, conds):
    main_sequencing_stack.pop()
    template.build_sequence(s, params, conds, main_block)

print(main_sequencing_stack)
[]

Surprisingly at a first glance, the sequencing stack of the main instruction block is empty afterwards although we are far from being done with the sequencing process. What happended here is that the call to LoopPulseTemplates build_sequence() method resulted in a call to build_sequence_loop of the corresponding condition object loop_condition. This is of type HardwareConditon, meaning that all possible execution paths must be translated into a hardware understandable format. Thus, a new InstructionBlock was instantiated into which the body of the loop will be sequenced. Accordingly, the remaining templates which represent the loops body are pushed to the specific sequencing stack of this new instruction block. In the main block we will simply find a CJMPInstruction (conditional jump instruction) to the new block.

In [19]:
print(main_block._InstructionBlock__instruction_list)
[<qctoolkit.pulses.instructions.CJMPInstruction object at 0x00000000076B1B00>]
In [20]:
# obtain a reference to the new InstructionBlock representing the body of the loop
loop_body_block = main_block._InstructionBlock__instruction_list[0].target.block
loop_body_stack = s._Sequencer__sequencing_stacks[loop_body_block]

The contents of the sequencing stacks are the following:

In [21]:
print(loop_body_stack) # print the sequencing stack for the loop body block
[(<qctoolkit.pulses.branch_pulse_template.BranchPulseTemplate object at 0x00000000076BCF98>, {'foo': <ConstantParameter 2>}, {'bcon': <qctoolkit.pulses.conditions.HardwareCondition object at 0x00000000076B11D0>, 'lcon': <qctoolkit.pulses.conditions.HardwareCondition object at 0x00000000076B1160>})]
Sequencing stacks after translating the loop template

Sequencing stacks after translating the loop template

Sequencer continues the sequencing process, until it cannot proceed for any instruction block currently under construction. Thus, although the stack for the main block is empty, we continue with the loop body block:

In [22]:
# get the topmost item from the sequencing stack
(template, params, conds) = loop_body_stack[-1]
# remove template from stack and translate it it does not require a stop
if not template.requires_stop(params, conds):
    loop_body_stack.pop()
    template.build_sequence(s, params, conds, loop_body_block)

print(loop_body_stack)
[]

Since we translated a BranchLoopTemplate with a HardwareConditon we end up with two new instructions blocks, one for the if-branch and one for the else-branch, with separate sequencing stacks. We also obtain corresponding jump instructions in the loop body block: A conditional jump into the if-branch, performed if the condition is fulfulled followed by an unconditional goto into the else-branch, if the conditional jump does not occur, i.e., the condition is not fullfilled.

In [23]:
print(loop_body_block._InstructionBlock__instruction_list)
[<qctoolkit.pulses.instructions.CJMPInstruction object at 0x00000000076CC668>, <qctoolkit.pulses.instructions.GOTOInstruction object at 0x00000000076CC6D8>]
In [24]:
# get references to if and else branches
if_branch_block = loop_body_block._InstructionBlock__instruction_list[0].target.block
else_branch_block = loop_body_block._InstructionBlock__instruction_list[1].target.block
if_branch_stack = s._Sequencer__sequencing_stacks[if_branch_block]
else_branch_stack = s._Sequencer__sequencing_stacks[else_branch_block]

The stacks now look as follows:

Sequencing stacks after translating the branch template

Sequencing stacks after translating the branch template

The table pulse templates pos_template and neg_template are translated in the usual manner, resulting in an EXECInstruction in the respective instruction blocks:

In [25]:
# translate if-branch stack
(template, params, conds) = if_branch_stack[-1]
if not template.requires_stop(params, conds):
    if_branch_stack.pop()
    template.build_sequence(s, params, conds, if_branch_block)

# translate else-branch stack
(template, params, conds) = else_branch_stack[-1]
if not template.requires_stop(params, conds):
    else_branch_stack.pop()
    template.build_sequence(s, params, conds, else_branch_block)

Afterwards, all stacks are empty

In [26]:
print(main_sequencing_stack)
print(loop_body_stack)
print(if_branch_stack)
print(else_branch_stack)
[]
[]
[]
[]

and we are left with four instruction blocks, two of which contains EXECInstructions while the rest only specifies control flow, that is, (conditional) jumps into other blocks. In an illustration, the blocks look like this:

Interconnected instruction block

Interconnected instruction block

Note that the returning unconditional gotos are not listed explicitely in the outputs above but always implicitely defined.

In the final step of the sequencing process, these blocks are now converted into a single sequence of instructions by embedding each blocks instructions into the instructions of the block jumping into it and then adapting the jump pointers. The final sequence is as follows:

In [27]:
instruction_sequence = main_block.compile_sequence()
print(instruction_sequence)
[<qctoolkit.pulses.instructions.CJMPInstruction object at 0x00000000076B1B00>, <qctoolkit.pulses.instructions.STOPInstruction object at 0x00000000076A7320>, <qctoolkit.pulses.instructions.CJMPInstruction object at 0x00000000076CC668>, <qctoolkit.pulses.instructions.GOTOInstruction object at 0x00000000076CC6D8>, <qctoolkit.pulses.instructions.GOTOInstruction object at 0x00000000076A7400>, <qctoolkit.pulses.instructions.EXECInstruction object at 0x00000000076A7A58>, <qctoolkit.pulses.instructions.GOTOInstruction object at 0x00000000076A7668>, <qctoolkit.pulses.instructions.EXECInstruction object at 0x00000000076A7E48>, <qctoolkit.pulses.instructions.GOTOInstruction object at 0x00000000076A7748>]
Resulting instruction sequence

Resulting instruction sequence

This instruction sequence indeed represents our desired behavior.

In [28]:
print(s.has_finished()) # really done?
True