1.4. Sequencing

1.4.1. Overview and Usage

Defining pulses using the pulse template definition class structures yields a tree structure of PulseTemplate objects. To obtain a concrete pulse that can be executed, parameters have to be replaced by corresponding values and the object structure has to be converted into a sequence of waveforms (and possibly triggered jump annotations) that the hardware drivers can comprehend. This process is called sequencing in the qctoolkit. It converts atomic pulse templates like TablePulseTemplate and FunctionPulseTemplate into a waveform representation by sampling voltage values along the time domain using the specified interpolation rules between table values or evaluating the defining function respectively. The tree structure arising from the use of SequencePulseTemplate, RepetitionPulseTemplate, ForLoopPulseTemplate, BranchPulseTemplate and LoopPulseTemplate is converted into an intermediate instruction language consisting of four instructions: Execute a waveform (EXECInstruction), an unconditional goto to another instruction (GOTOInstruction), a conditional jump to another instruction (JMPInstruction) and a stop command, halting execution (STOPInstruction).

This approach was inspired by translation of syntax trees to machine instructions in modern compilers and necessitated by the fact that the hardware requires sequential commands rather than convoluted object structures. The output of the sequencing process is a set of waveforms and a sequence of instructions. These will later be interpreted by hardware drivers which will configure the specific devices accordingly (assuming these are capable of the required functionality). If BranchPulseTemplate and LoopPulseTemplate are not used, the compiled instruction sequence will consist only of execution instructions followed by a stop instruction, which represents a simple sequence of waveforms to play back.

The sequencing process is performed by a Sequencer and started with a call to Sequencer.build(). Sequencer maintains a stack of SequencingElement objects (i.e. pulse templates) that still need to be translated. Pushing a pulse template to the stack using Sequencer.push() will cause Sequencer to translate it next. Translation works as follows: Sequencer pops the first element from the stack and calls its SequencingElement.build_sequence() method. In the case of a TablePulseTemplate, this adds an EXECInstruction referring to a TableWaveform to the instruction sequence. In the case of a SequencePulseTemplate, this simply adds all subtemplates to the sequencing stack such that they are translated next. FunctionPulseTemplate and RepetitionPulseTemplate act very similarly. BranchPulseTemplate and LoopPulseTemplate behave in a more complicated way due to their branching ability. See the section on implementation details below.

The sequencing process can be interrupted at any point, e.g., if some parameter value depends on measurements that are to be made in the preceding part of the pulse. In this case, the method SequencingElement.requires_stop() of the first stack element will return true. Sequencer then stops the translation and returns the instruction sequence generated so far. This can then be executed and measurements can be made. Afterwards, the sequencing process can be invoked again. Sequencer will resume where it was interrupted previously (with the first item that remained on the stack). Sequencer.has_finished() allows to check, whether the sequencing process was completed.

1.4.2. Sequencing of Conditional Branching

Software and hardware conditions result in different instruction sequences generated by the sequencing process: Hardware conditions will produce instructions for all possible branches with branching instructions corresponding to the triggers specified by the HardwareCondition instance. The selection of the correct branch is then made by the hardware. Contrary, software conditions will only produce instructions for the branch selected by the condition (the hardware will then never know of potential other execution paths). In this case, no branching instructions will be used. This enables usage of branching even on hardware that does not support jumps with the disadvantage of being not real-time capable (cf. conditional branching.).

1.4.3. Implementation Details and Algorithm Walkthrough

The implementation sequencing algorithm itself is spread over all participating classes. The Sequencer maintains a stack of pulse templates which are to be translated (the sequencing stack) and implements the overall control flow of the algorithm: It proceeds to remove the first template from the stack and call PulseTemplate.build_sequence() until either the stack is empty or the first element cannot be translated. Afterwards it embeds all InstructionBlock s into one single InstructionSequence and resolves pointers in JMPInstruction s. Details on why this is necessary will follow below.

A crucial component of the sequencing process are the methods PulseTemplate.requires_stop() and PulseTemplate.build_sequence() (defined in the abstract interface SequencingElement). These accept a mapping of parameters and conditions as well as the InstructionBlock that is currently under construction and implement the specific functionality of the corresponding template required in the sequencing process. Examples of this were already briefly stated above but shall be elaborated more in the following.

Considering the TablePulseTemplate, the required result of the sequencing process is a waveform representation of the pulse defined by the template and an instruction that this waveform shall be executed by the hardware. If any parameter cannot be evaluated at the time, the sequencing process should stop since the waveform cannot be created. Consequently, TablePulseTemplate.requires_stop() is implemented to return true if any of the parameters required by the template cannot be evaluated, causing the Sequencer to interrupt the sequencing process before calling TablePulseTemplate.build_sequence(). TablePulseTemplate.build_sequence(), evaluates the required parameters, creates the waveform and adds a corresponding EXECInstruction to the InstructionBlock. Since conditions are only relevant for branching, the corresponding mapping passed into the method is ignored in the TablePulseTemplate implementation.

For a SequencePulseTemplate we require that all templates forming this sequence will be translated, i.e., the translation of the SequencePulseTemplate is the sequential translation of all contained subtemplates. To this end, SequencePulseTemplate.build_sequence() does not perform any translation by itself but simply pushes all subtemplates to the sequencing stack, only taking care to map parameters if necessary. Conditions are ignored as in the case of TablePulseTemplate. Since SequencePulseTemplate does not need to evaluate any parameters by itself, SequencePulseTemplate.requires_stop() always returns false.

Finally, for a LoopPulseTemplate we expect one of the following behaviours depending on whether the looping condition is evaluated hardware- or software-based: In the first case, the template representing the loop body should be translated and wrapped with conditional jumping instructions which will cause the hardware device to repeat the resulting waveforms as long as the condition holds. In the second case, the condition must be evaluated by the software and, if it is true, the loop body must be translated and executed in-place, i.e. without any jumps. Afterwards, the execution must stop to reevaluate the condition in the software and decide whether the loop body must be executed again. Since these different behaviours for hardware- and software-based evaluation are similar for loops and branches, they are not directly implemented in the LoopPulseTemplate and BranchPulseTemplate classes but in the corresponding implementations of Condition. The task of LoopPulseTemplate.build_sequence() is thus only to obtain the correct instance of Condition from the provided conditions mapping and delegate the call to Condition.build_sequence_loop(). Depending on whether the condition is a HardwareCondition or SoftwareCondition instance, Condition.build_sequence_loop() will push the required templates to the sequencing stack and generate the corresponding jumping instructions.

During the sequencing process, instructions are not directly stored in a fixed sequence. This would be impractical since during the process it is not clear how many alternative paths due to branching and looping will arise and thus where to place the corresponding instructions in a single sequence. Instead, each branch or loop body is represented by an InstructionBlock comprising all execution instructions in this branch as well as potential jump instructions into other blocks. This allows independent construction of blocks during the sequencing process. The Sequencer maintains separate sequencing stacks for each block which allows to continue translating other blocks if a particular block encounters a template which requires a stop. The block into which instructions have to be placed is passed as an argument into PulseTemplate.build_sequence(). With the exception of the main instruction block, which represents the entry point of instruction execution, every block is entered by a conditional jump instruction in another block and ends with an unconditional jump/goto back to the next instruction of that block. When the sequencing process finished, Sequencer embeds all instruction blocks into a single sequence and resolves the instruction pointers of the jump instructions to positions in this sequence instead of references to other blocks. The result is a sequence of instructions represented as InstructionSequence in which no blocks remain and executions paths are solely represented by jumps to instruction positions.

For two step-by-step examples on how the sequencing process proceeds, see examples/09DetailedSequencingWalkthrough.ipynb.

Note

Provide an exemplary sequencing run