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.
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>})]
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 EXECInstruction
s:
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>})]
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 LoopPulseTemplate
s 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>})]
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:
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:
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>]
This instruction sequence indeed represents our desired behavior.
In [28]:
print(s.has_finished()) # really done?
True