Welcome to qc-toolkit’s documentation!

Contents:

Concepts

This section will explain the fundamental design concepts of the qctoolkit.

Pulse Templates

The qctoolkit represents pulses as abstract pulse templates. A pulse template can be understood as a class of pulses that share a similar structure but differ in the concrete amplitude or duration of voltage levels. To this end, pulse templates are parametrizable. Pulse templates are also designed to feature easy reusability of existing templates and conditional execution based on hardware triggers, if supported by the devices.

There are 6 types of different pulse template classes, briefly explained in the following. TablePulseTemplate and FunctionPulseTemplate are used to define the atomic building blocks of pulses in the following ways: TablePulseTemplate allows the user to specify pairs of time and voltage values and choose an interpolation strategy between neighbouring points. FunctionPulseTemplate will accept any mathematical function that maps time to voltage values. All other pulse template variants are then used to construct arbitrarily complex pulses by combining existing ones into new structures: SequencePulseTemplate enables the user to specify a sequence of existing pulse templates (subtemplates) and modify parameter values using a mapping function. RepetitionPulseTemplate is used to simply repeat one existing pulse template a given (constant) number of times. BranchPulseTemplate and LoopPulseTemplate implement conditional execution if supported. All of these pulse template variants can be similarly accessed through the common interface declared by PulseTemplate. [1] [2]

Each pulse template can be stored persistently in a human-readable JSON file. Read more about serialization.

Parameters

As mentioned above, all pulse templates may contain parameters. TablePulseTemplate allows parameter references as table entries on the time and voltage domain. These are represented as ParameterDeclaration objects which are identified by a unique name and can impose lower and upper boundaries to the expected parameter value as well as a default value. SequencePulseTemplate allows to specify a set of new parameter declarations and a mapping of these to the parameter declarations of its subtemplates. This allows renaming of parameters, e.g., to avoid name clashes if several subtemplates declare similarly named parameters. The mapping also allows mathematical transformation of parameter values, such that values that are passed to subtemplates can be obtained by deriving them from one or more other parameter values passed to the SequencePulseTemplate. RepetitionPulseTemplate, LoopPulseTemplate and BranchPulseTemplate will simply pass parameters to their subtemplates without modifying them in any way.

The mathematical expressions (for parameter transformation or as the function of the FunctionPulseTemplate) are encapsulated into an Expression class which wraps existing python packages that are able to parse and evaluate expressions given as strings such as py_expression_eval and numexpr.

Obtaining a Concrete Pulse

To obtain a pulse ready for execution on the hardware from a pulse template, the user has to specify parameter values (if parameters were used in the pulse templates in question). In the simplest case, parameters are constant values that can be provided as plain float values. Other cases may require parameter values to be computed based on some measurement values obtained during preceding executions. If so, a subclass of the Parameter class which performs this computations when queried for a value can be provided. In order to translate the object structures that encode the pulse template in the software into a sequential representation of the concrete pulse with the given parameter values that is understandable by the hardware, the sequencing process has to be invoked. During this process, all parameter values are checked for consistency with the boundaries declared by the parameter declarations and the process is aborted if any violation occurs. Read more about the sequencing process.

Relevant Examples

Examples demonstrating the use of pulse templates and parameters are Modelling a Simple TablePulseTemplate, Combining PulseTemplates Using SequencePulseTemplate and Modelling Pulses Using Functions And Expressions.

Footnotes

[1]Regarded as objects in the programming language, each pulse template is a tree of PulseTemplate objects, where the atomic templates (TablePulseTemplate and FunctionPulseTemplate objects) are the leafs while the remaining ones form the inner nodes of the tree.
[2]The design of the pulse template class hierarchy is an application of the Composite Pattern.

Serialization

Serialization and deserilization mechanisms were implemented to enable persistent storage and thus reusability of pulse template definitions. Currently, the serialization format is a plain text document containing JSON formatted data. [1]

Serialization was constructed in a way that allows that a given pulse template may refer to subtemplates which are used by several different parent templates (or more than once in one) such as, e.g., the measurement pulse. These should then be stored in a separate file and referenced by a unique identifier in all parent templates to avoid unnecessary redundancy. On the other hand, there might be subtemplates which are only relevant to their parent and thus should be embedded in its serialization to avoid creating a multitude of files that are meaningless to a user. To allow the serialization process to make this distinction, each pulse template (or other serializable object) provides an optional identifier (which can be set by the user via the constructor for all pulse template variants). If an identifier is present in a pulse template, it is stored in a separate file. If not, it is embedded in its parent’s serialization.

The implementation of (de)serialization features Serializer class and a Serializable interface. Every class that implements the latter can be serialized and thus stored as a JSON file. Currently, this is the case for all PulseTemplate variants as well as the ParameterDeclaration class. Additionally, the StorageBackend interface abstracts the actual storage backend. While currently there only exists a single implementation of this interface, namely the FileSystemStorageBackend, this allows to support, e.g., database storage, in the future.

The essential methods of Serializer are Serializer.serialize() and Serializer.deserialize(). Serializer.serialize() serializes a serializable object (i.e., any object of a class that implements/derives from Serializable) in a recursive process: It invokes the Serializable.get_serialization_data() method of the provided serializable object, which in turn might invoke the Sequencer to obtain a serialization for complex embedded data (such as a ParameterDeclaration in a TablePulseTemplate). In the end, a dictionary representation of the object is obtained which is then converted into a JSON string using built-in Python functionality. The JSON representation is finally stored using a given StorageBackend. Deserialization (Serializer.deserialize()) works analogously and is thus not explained in detail here.

For an example of how to use serialization to store and load pulse templates, see Storing Pulse Templates: Serialization in the examples section.

Implementing a Serializable Class

To make any new class serializable, it must derive from the Serializable and implement the methods Serializable.get_serialization_data(), Serializable.deserialize() and the Serializable.identifier property.

If class objects should be stored in a separate file, the identifier must be a non-empty string. If, on the other hand, class objects should be embedded into their parent’s serialization (as is the case for, e.g., ParameterDeclaration), identifier must be None.

The method serialize should return a dictionary of native Python types containing all relevant data. If the class has members that are not native Python types but must be serialized, they must be serializable and the serialize method can obtain their serialization as the return value of Serializer._serialize_subpulse() and embed it in its result. The dictionary returned by serialize should not include the identifier in the returned dictionary.

The method deserialize is invoked with all key-value pairs created by a call to serialize as keyword arguments as well as an additional identifier keyword argument (which may be None) and must return a valid corresponding class instance.

The following may serve as a simple example:

from qctoolkit.serialization import Serializable, Serializer
from qctoolkit.pulses import PulseTemplate
from typing import Any, Dict, Optional

class Foo(Serializable):

    def __init__(self, template: PulseTemplate, identifier: Optional[str]=None) -> None:
        self.__template = template
        self.__identifier = identifier

    @property
    def identifier(self) -> Optional[str]:
        return self.__identifier

    def get_serialization_data(self, serializer: Serializer) -> Dict[str, Any]:
        return dict(template=serializer._serialize_subpulse(self.__template))

    @staticmethod
    def deserialize(serializer: Serializer, template: Dict[str, Any], identifer: Optional[str]=None) -> Serializable:
        return Foo(serialzer.deserialize(template), identifier=identifier)

Footnotes

[1]After some discussion of the format in which to store the data, JSON files were the favored solution. The main competitor were relational SQL databases, which could provide a central, globally accessible pulse database. However, since pulses are often changed between experiments, a more flexible solution that can be maintained by users without database experience and also allows changes only in a local environment was desired. Storing pulse templates in files was the obvious solution to this. This greatest-simplicity-requirement was also imposed on the data format, which thus resulted in JSON being chosen over XML or other similar formats. An additional favorable argument for JSON is the fact that Python already provides methods that convert dictionaries containing only native python types into valid JSON and back.

Conditional Branching

The qctoolkit was designed to model conditional branching in pulse execution using the classes BranchPulseTemplate and LoopPulseTemplate of the pulse template definition classes. The software is intended to support use of trigger-based hardware jumps between waveforms as well as software-based evaluation of branching conditions, if the hardware does not support triggers or highly sophisticated evaluation of measured data is required. These options are represented as instances of the Condition class: BranchPulseTemplate and LoopPulseTemplate both contain unique identifiers (chosen by the user) of a condition. In the beginning of the sequencing process, the user provides a dictionary of Condition instances which are used to determine how the conditions for each template are evaluated. Currently, two different subclasses of Condition exist: SoftwareCondition represents a software-evaluated condition and accepts a function reference to any evaluation function which must return a boolean value indicating whether or not the condition is fulfilled or None, if it cannot yet be evaluated; HardwareCondition represents trigger-based hardware evaluation and basically stores a reference to a trigger which will later be evaluated by the specific hardware driver to set up the device accordingly. Note that software-based evaluation will interrupt the sequencing process and thus the execution of the pulse if the evaluation cannot be performed during the sequencing run, e.g., if the evaluation of the condition is based on measurement data made during pulse execution. In this case, the pulse instruction sequence is only generated up to the branching and must be re-invoked later on, after executing this first part and obtaining the required data. Software-based evaluation is thus not feasible when high performance is required.

Future Work

Currently, there is no detailed concept on hardware abstraction and thus no meaningful representation of triggers and no hardware driver implementation that configures any device. This is still an open task.

It is quite common for hardware to allow triggers that are represented not only a boolean signal but, e.g., any 8-bit signal, thus enabling more than two branching options. While this could still be represented by the current classes by nesting BranchPulseTemplate objects and configuring triggers appropriately, the implementation of a template class which acts like a C-style switch statement might be a worthwhile consideration.

Sequencing

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 TablePulseTemplate and FunctionPulseTemplate objects 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, 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.

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. Read more on conditional branching.).

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 Detailed Sequencing Walkthrough.

Note

Provide an exemplary sequencing run

Examples

All examples are provided as static text in this documentation and, additionally, as interactive jupyter notebooks accessible by running jupyter notebook in the /doc/source/examples directory of the source tree.

Modelling a Simple TablePulseTemplate

This example demonstrates how to set up a simple TablePulseTemplate.

The pulse we want to model using the qctoolkit

The pulse we want to model using the qctoolkit

Assume we want to model a pulse as depicted by the figure above. Since the structure is relatively simple and relies only on a few critical points between which the values are interpolated (indicated by the crosses in the figure), we will do so by using a TablePulseTemplate and setting values at appropriate times. First, let us instantiate a TablePulseTemplate object:

In [2]:
from qctoolkit.pulses import TablePulseTemplate

template = TablePulseTemplate(identifier='foo')

For our first try, let’s just fix some values for the parameters. Let us set \(t_a\) = 2, \(v_a\) = 2, \(t_b\) = 4, \(v_b\) = 3, \(t_{end}\) = 6. Our pulse then holds a value of 0 for 2 units of time, then jumps to 2 and subsequently ramps to 3 over the next 2 units of time. Finally, it returns to holding 0 for another 2 units of time. The supporting points for the table are thus (0,0), (2,2), (4,3), (6,0). We add these to our TablePulseTemplate object template with the correct interpolation strategies as follows:

In [2]:
template.add_entry(0, 0)
template.add_entry(2, 2, interpolation='hold')
template.add_entry(4, 3, interpolation='linear')
template.add_entry(6, 0, interpolation='jump')

Note that we could omit the first instruction: If the time value of the first call to add_entry is greater than zero, a starting entry (0,0) is automatically set. Note further that the interpolation set for an entry always applies to the range from the previous entry to the new entry. Thus, the value for the first entry is always ignored. The default value for interpolation is ‘hold’. ‘hold’ and ‘jump’ differ in that ‘hold’ will hold the previous value until the new entry is reached while ‘jump’ will immediately assume the value of the new entry.

We plot template to see if everything is correct (ignore the matplotlib warning here):

In [5]:
%matplotlib inline
from qctoolkit.pulses import plot

plot(template, sample_rate=100)
C:\Anaconda3\lib\site-packages\matplotlib\figure.py:387: UserWarning: matplotlib is currently using a non-GUI backend, so cannot show the figure
  "matplotlib is currently using a non-GUI backend, "
_images/examples_00SimpleTablePulse_5_1.png

Alright, we got what we wanted.

Note that the time domain in pulse defintions does not correspond to any fixed real world time unit. The mapping from a single time unit in a pulse definition to real time in execution is made by setting a sample rate when converting the pulse templates to waveforms. For more on this, see The Sequencing Process: Obtaining Pulse Instances From Pulse Templates.

Introducing Parameters

Now we want to make the template parameterizable. This allows us to reuse the template for pulses with similar structure. Say we would like to have the same pulse, but the intermediate linear interpolation part should last 4 units of time instead of only 2. Instead of creating another template with hardcoded values, we instruct the TablePulseTemplate instance to rely on parameters.

In [6]:
param_template = TablePulseTemplate()
param_template.add_entry('ta', 'va', interpolation='hold')
param_template.add_entry('tb', 'vb', interpolation='linear')
param_template.add_entry('tend', 0, interpolation='jump')

Instead of using numerical values, we simply insert parameter names in our calls to add_entry. You can use any combination of numerical values or parameters names here. Note that we also gave our object the optional identifier ‘foo’. Our param_template thus now defines a set of parameters.

In [7]:
print(param_template.parameter_names)
{'va', 'ta', 'tend', 'vb', 'tb'}

We now have a pulse template that we can instantiate with different parameter values, which we simply provide as a dictionary. To achieve the same pulse as above:

In [8]:
%matplotlib inline
parameters = {'ta': 2,
              'va': 2,
              'tb': 4,
              'vb': 3,
              'tend': 6}
plot(param_template, parameters, sample_rate=100)
C:\Anaconda3\lib\site-packages\matplotlib\figure.py:387: UserWarning: matplotlib is currently using a non-GUI backend, so cannot show the figure
  "matplotlib is currently using a non-GUI backend, "
_images/examples_00SimpleTablePulse_11_1.png

To instantiate the pulse with longer intermediate linear interpolation we now simply adjust the parameter values without touching the param_template itself:

In [9]:
%matplotlib inline
parameters = {'ta': 2,
              'va': 2,
              'tb': 6,
              'vb': 3,
              'tend': 8}
plot(param_template, parameters, sample_rate=100)
C:\Anaconda3\lib\site-packages\matplotlib\figure.py:387: UserWarning: matplotlib is currently using a non-GUI backend, so cannot show the figure
  "matplotlib is currently using a non-GUI backend, "
_images/examples_00SimpleTablePulse_13_1.png

Combining PulseTemplates Using SequencePulseTemplate

In this example we will use the SequencePulseTemplate class to build a sequence of two of the simple table pulses defined in Modelling a Simple TablePulseTemplate. We will also map the parameters such that we can individually adapt values in either of the two subtemplates.

We start with the defintion of the TablePulseTemplate:

In [1]:
from qctoolkit.pulses import TablePulseTemplate

template = TablePulseTemplate(identifier='foo')
template.add_entry('ta', 'va', interpolation='hold')
template.add_entry('tb', 'vb', interpolation='linear')
template.add_entry('tend', 0, interpolation='jump')

Constructing a sequence of two of these requires us to set up a SequencePulseTemplate instance which expects a list of subtemplates, a set of external parameters and corresponding parameters mappings:

In [2]:
from qctoolkit.pulses import SequencePulseTemplate

external_parameters = ['ta', 'tb', 'tc', 'td', 'va', 'vb', 'tend']
first_mapping = {
    'ta': 'ta',
    'tb': 'tb',
    'va': 'va',
    'vb': 'vb',
    'tend': 'tend'
}
second_mapping = {
    'ta': 'tc',
    'tb': 'td',
    'va': 'vb',
    'vb': 'va + vb',
    'tend': '2 * tend'
}
sequence = SequencePulseTemplate([(template, first_mapping),
                                  (template, second_mapping)],
                                 external_parameters)

Our sequence now exposes the parameters declared in the external_parameters set:

In [3]:
print(sequence.parameter_names)
frozenset({'td', 'tc', 'ta', 'vb', 'va', 'tb', 'tend'})

The mappings are constructed such that the first occurance of our table template will receive its parameters without any modification. For the second, however, we renamed the parameters: The ‘tc’ parameter of the sequence is mapped to the ‘ta’ parameter of the table template instance; ‘td’ is mapped to ‘tb’ of the subtemplate, ‘vb’ (of the sequence template) to ‘va’ (of the subtemplate). The value for ‘vb’ of the subtemplate is computed as the sum of the values of ‘va’ and ‘vb’ passed to the sequence and the value for ‘tend’ is double before passing it on. We can do a variety of transformations here, allowing us to modify how parameters are handled to add additional computations in composed pulse templates or simply to avoid name collisions between subtemplate parameters.

Let’s throw in some values and plot our sequence:

In [3]:
%matplotlib inline
from qctoolkit.pulses import plot

parameters = {'ta': 2,
              'va': 2,
              'tb': 4,
              'vb': 3,
              'tc': 5,
              'td': 11,
              'tend': 6}
plot(sequence, parameters, sample_rate=100)
C:\Anaconda3\lib\site-packages\matplotlib\figure.py:387: UserWarning: matplotlib is currently using a non-GUI backend, so cannot show the figure
  "matplotlib is currently using a non-GUI backend, "
_images/examples_01SequencePulse_7_1.png
In [ ]:

Modelling Pulses Using Functions And Expressions

Assume we want to model a pulse that represents a damped sine function. While we could, in theory, do this using TablePulseTemplates by piecewise linear approximation (cf. Modelling a Simple TablePulseTemplate), this would be a tedious endeavor. A much simpler approach presents itself in the form of the FunctionPulseTemplate class of the qctoolkit. Like the TablePulseTemplate, a FunctionPulseTemplate represents an atomic pulse which will be converted into a waveform for execution. The difference between both is that FunctionPulseTemplate accepts a mathematical expression which is parsed and evaluated using py_expression_eval to sample the waveform instead of the linear interpolation between specified supporting points as it is done in TablePulseTemplate.

To define the sine function pulse template, we can thus do the following:

In [2]:
from qctoolkit.pulses import FunctionPulseTemplate

template = FunctionPulseTemplate('exp(-t/2)*sin(2*t)', '2*3.1415')

%matplotlib inline
from qctoolkit.pulses import plot

plot(template, sample_rate=100)
C:\Anaconda3\lib\site-packages\matplotlib\figure.py:387: UserWarning: matplotlib is currently using a non-GUI backend, so cannot show the figure
  "matplotlib is currently using a non-GUI backend, "
_images/examples_02FunctionPulse_1_1.png

The first argument to FunctionPulseTemplate‘s constructor is the string representation of the formula that the pulse represents. The second argument is used to compute the length of the pulse. In this case, this is simply a constant expression. Refer to py-expression-eval’s documentation to read about the usable operators and functions in the expressions.

The t is reserved as the free variable of the time domain in the first argument and must be present. Other variables can be used at will and corresponding values have to be passed in as a parameter when instantiating the FunctionPulseTemplate:

In [9]:
param_template = FunctionPulseTemplate('exp(-t/lambda)*sin(phi*t)', 'duration')

%matplotlib inline
from qctoolkit.pulses import plot

plot(param_template, {'lambda': 4, 'phi': 8, 'duration': 4*3.1415}, sample_rate=100)
C:\Anaconda3\lib\site-packages\matplotlib\figure.py:387: UserWarning: matplotlib is currently using a non-GUI backend, so cannot show the figure
  "matplotlib is currently using a non-GUI backend, "
_images/examples_02FunctionPulse_3_1.png
In [ ]:

Storing Pulse Templates: Serialization

So far, we have constructed new pulse templates in code for each session (which were discarded afterwards). We now want to store them persistently in the file system to be able to reuse them in later sessions. For this, qctoolkit offers us serialization and deserialization using the Serializer and StorageBackend classes.

Serializing Atomic Templates

Storing
In [1]:
from qctoolkit.pulses import TablePulseTemplate
from qctoolkit.serialization import Serializer, FilesystemBackend

anonymous_table = TablePulseTemplate()
anonymous_table.add_entry('ta', 'va', interpolation='hold')
anonymous_table.add_entry('tb', 'vb', interpolation='linear')
anonymous_table.add_entry('tend', 0, interpolation='jump')

backend = FilesystemBackend("./serialized_pulses")
serializer = Serializer(backend)

serializer.serialize(anonymous_table)

Assuming that we, again, have the TablePulseTemplate from Modelling a Simple TablePulseTemplate we instantiate a Serializer object to store it. Serializer requires a StorageBackend instance which represents the actual data storage. We provide a FilesystemBackend instance which will store data in the directory ./serialized_pulses. With this setup, storing the pulse template simply means invoking the serialize method of the Serializer instance and passing in our template. This will store a JSON representation of the object in the specified storage. Since we have not provided any file name, Serializer chooses the file name as `main <serialized_pulses/main>`__.

To specify a name, we can provide an identifier as an argument to the constructor of any pulse template:

In [2]:
identified_table = TablePulseTemplate(identifier='table_template')
identified_table.add_entry('ta', 'va', interpolation='hold')
identified_table.add_entry('tb', 'vb', interpolation='linear')
identified_table.add_entry('tend', 0, interpolation='jump')

serializer.serialize(identified_table)

This will create a file named `table_template <serialized_pulses/table_template>`__ with the same contents as above.

Loading

To load a previously stored pulse template, we use the method Serializer.deserialize with the file name. The following code will load a very simple table pulse template which ramps from 0 to 20 over a duration of 4 units of time and which is stored under the name `stored_template <serialized_pulses/stored_template>`__:

In [3]:
loaded_template = serializer.deserialize('stored_template')

%matplotlib inline
from qctoolkit.pulses import plot

plot(loaded_template, sample_rate=100)
C:\Anaconda3\lib\site-packages\matplotlib\figure.py:387: UserWarning: matplotlib is currently using a non-GUI backend, so cannot show the figure
  "matplotlib is currently using a non-GUI backend, "
_images/examples_03Serialization_5_1.png

Serializing Composite Templates

Serializing atomic templates (TablePulseTemplate and FunctionPulseTemplate) is a straightforward task. However, when storing composite templates that refer to subtemplates, e.g., SequencePulseTemplate, one must decide whether to embed the subtemplates into the composite’s serialization or to store them separately. The latter one will be preferred if the subtemplate is used in several composite templates. Sequencer will decide whether to store a subtemplate separately based on the fact whether or not it provides an identifier: Subtemplates without an identifier will be embedded.

In [4]:
from qctoolkit.pulses import SequencePulseTemplate

mapping = {
    'ta': '1',
    'tb': '2',
    'va': '5',
    'vb': '0',
    'tend': '5'
}
sequence1 = SequencePulseTemplate([(anonymous_table, mapping)], {}, identifier='sequence_embedded')
serializer.serialize(sequence1)

sequence2 = SequencePulseTemplate([(identified_table, mapping)], {}, identifier='sequence_referenced')
serializer.serialize(sequence2)

The above code snippet creates two SequencePulseTemplates consisting of only one table subtemplate (one of those defined above respectively). The anonymous_table is used in sequence1. Since it has no identifier, it is embedded in the serialization: `sequence_embedded <serialized_pulses/sequence_embedded>`__.

In contrast, the table subtemplate in sequence2 has an identifier, so the serialization of sequence2 will contain only a reference to the serialization of identified_table: `sequence_referenced <serialized_pulses/sequence_referenced>`__.

Storage Backends

So far, we have only used the FilesystemBackend to store pulse template directly in some folder. However, the abstraction of the StorageBackend theoretically allows us to implement other backends, e.g., a SQL database, without changing the Serializer.

Additionally, the class CachingBackend can be used to decorate and add caching functionality to any other StorageBackend instance to speed up loading of frequently accessed templates as follows:

In [5]:
from qctoolkit.serialization import CachingBackend
cached_serializer = Serializer(CachingBackend(FilesystemBackend("./serialized_pulses")))
In [ ]:

The Sequencing Process: Obtaining Pulse Instances From Pulse Templates

In the previous examples, we have modelled pulses using the basic members of qctoolkit’s PulseTemplate class hierarchy. However, these are only templates (or classes) of pulses and may contain parameters so that they cannot be run directly on hardware. First, we have to instantiate concrete waveforms and instructions in which order these will be run. This process is called sequencing in the qctoolkit. It involves the instantiations of parameters with concrete values and the sampling of atomic pulse templates (TablePulseTemplate and FunctionPulseTemplate) to obtain waveforms. Sequencing is performed by the Sequencer class. This example will explore how.

First, let’s assume we have the PulseTemplates from the examples Modelling a Simple TablePulseTemplate and Modelling Pulses Using Functions And Expressions composed in a SequencePulseTable (cf. Combining PulseTemplates Using SequencePulseTemplate) with some parameter mappings:

In [5]:
%matplotlib inline
from qctoolkit.pulses import TablePulseTemplate, FunctionPulseTemplate, SequencePulseTemplate, plot

table_template = TablePulseTemplate()
table_template.add_entry('ta', 'va', interpolation='hold')
table_template.add_entry('tb', 'vb', interpolation='linear')
table_template.add_entry('tend', 0, interpolation='jump')

function_template = FunctionPulseTemplate('exp(-t/lambda)*sin(phi*t)', 'duration')

table_parameter_mapping = {
    'ta': 'ta',
    'tb': 'ta + duration',
    'tend': '15',
    'va': 'va',
    'vb': '0'
}
function_parameter_mapping = {
    'lambda': 'lambda',
    'phi': 'phi',
    'duration': 'duration'
}
sequence_template = SequencePulseTemplate([(table_template, table_parameter_mapping),
                                           (function_template, function_parameter_mapping)],
                                          {'ta', 'duration', 'va', 'lambda', 'phi'})


plot(sequence_template, {'lambda': 4, 'phi': 8, 'duration': 4*3.1415, 'ta': 1, 'va': 2}, sample_rate=100)
C:\Anaconda3\lib\site-packages\matplotlib\figure.py:387: UserWarning: matplotlib is currently using a non-GUI backend, so cannot show the figure
  "matplotlib is currently using a non-GUI backend, "
_images/examples_04Sequencing_1_1.png

While the plot illustrates how the pulse will look like for the given parameter values, the object structure we created is only an abstract representation of pulses with this structure. (This is the reason we always have to provide specific parameters values for plotting.) To convert this tree-like object structure into something the hardware understands, we will use the Sequencer class:

In [7]:
from qctoolkit.pulses import Sequencer

sequencer = Sequencer()

sequencer.push(sequence_template, {'lambda': 4, 'phi': 8, 'duration': 4*3.1415, 'ta': 1, 'va': 2})
instruction_sequence = sequencer.build()
print(instruction_sequence)
[<qctoolkit.pulses.instructions.EXECInstruction object at 0x000000000734FEF0>, <qctoolkit.pulses.instructions.EXECInstruction object at 0x00000000072E3E48>, <qctoolkit.pulses.instructions.STOPInstruction object at 0x00000000072E3E10>]

Using the push method of Sequencer, we add PulseTemplate objects to the sequencing stack, i.e., the last pushed object will be sequenced first. We then invoke the build method to start the sequencing process, which will process all objects on the stack and return a sequence of hardware instructions. As you can see in the output of the above code snippet, the Sequencer created a sequence of two EXECInstructions and a STOPInstruction. The two EXECInstructions refer to waveforms obtained by sampling the atomic template objects table_template and function_template of which the sequence_template we’ve sequenced is composed.

These waveforms are represented by objects of the Waveform class, which exposes a method sample, accepting an array of sample points in the time domain and returning an equally long array of sample values. The choice of the sample points passed into the sample method together with the sample rate of the playback device determine the duration of a time unit in a pulse template. For example, a pulse template with a length of 1 time unit that is sampled using 1000 equidistant sample points will play for 1 µs on a playback device with a sample rate of 1 GHz.

After the sequencing process is complete, the instruction sequence will be passed to a hardware-dependent interpreter which will configure the corresponding device accordingly and perform the sampling of the waveform depending on the device’s sample rate and typically a parameter indicating the real-time duration of a pulse template time unit.

Note that the plot function internally employs sequencing and then simply plots the generated waveforms.

Side Note: Instructions

The generated instruction sequence will consist of some of the following instructions: - EXECInstruction: execute a waveform - STOPInstruction: stop the execution - JUMPInstruction: jump to some instruction in the instruction sequence based on some trigger/condition - GOTOInstruction: go to some instruction in the instruction sequence

The latter two will only be generated when hardware-based conditional branching is used in LoopPulseTemplate and BranchPulseTemplate. This was the main motivator for the usage of such an instruction sequence rather than just compling one large waveform. Using only the PulseTemplates discussed so far, only EXECInstructions and a final STOPInstruction will be output by the Sequencer.

In [ ]:

More Sophisticated Parameters

This example assumes that you are familiar with the sequencing process. If you need a refresher on this, have a look at The Sequencing Process: Obtaining Pulse Instances From Pulse Templates first.

Attention/Broken: During the creation of this example some implementation errors were found in the qctoolkit. Due to time constraints, these were not fixed immediately, leaving this example to be slightly flawed. However, in order to demonstrate the underlying principles, this example is published in its current form. Annotations like this mark where the current behavior of the qctoolit diverges from the intended one.

So far we have only considered constant parameter values. Now assume that we need to derive the value for a parameter based on some measurements we’ve made during the execution of previous parts of a composite pulse. For example, let the pulse we want to execute be constructed as follows:

In [1]:
%matplotlib inline
from qctoolkit.pulses import TablePulseTemplate, SequencePulseTemplate, plot

init_template = TablePulseTemplate()
init_template.add_entry(2, 5)
init_template.add_entry(4, -5)
init_template.add_entry(6, 0)
init_template.add_entry(8, 0)

measurement_template = TablePulseTemplate(measurement=True)
measurement_template.add_entry(0, 2)
measurement_template.add_entry(4, 0)

dependent_template = TablePulseTemplate()
dependent_template.add_entry(2, 0)
dependent_template.add_entry(5, 'v', 'linear')
dependent_template.add_entry(10, 0, 'linear')

sequence_template = SequencePulseTemplate([(init_template, {}),
                                           (measurement_template, {}),
                                           (dependent_template, {'v': 'v'}),
                                           (init_template, {})
                                          ], {'v'})

plot(sequence_template, {'v': 1}, sample_rate=100)
C:\Anaconda3\lib\site-packages\matplotlib\figure.py:387: UserWarning: matplotlib is currently using a non-GUI backend, so cannot show the figure
  "matplotlib is currently using a non-GUI backend, "
_images/examples_05Parameters_1_1.png

Now we want to let the value of parameter v depend somehow on the measurement we make between time 8 and 12 (assuming we have a way to obtain measurement data, which is currently not the case (work in progress)). Thus we need to execute the first part of the pulse, then compute the parameter value and execute the remainder. We can do so be encapsulating the computation of the parameter value in a custom subclass of Parameter. Assuming, for simplicity, that we have some measurement_manager object which we can query whether or not the measurement has been made (is_measurement_available()) and what data was obtained (get_data()) and that the value of v shall simply be twice the measured data, this subclass might look like this:

In [2]:
from qctoolkit.pulses import Parameter

class MeasurementDependentParameter(Parameter):

    def __init__(self, measurement_manager) -> None:
        self.measurement_manager = measurement_manager

    @property
    def requires_stop(self) -> bool:
        return not self.measurement_manager.is_measurement_available()

    def get_value(self) -> float:
        return 2*(self.measurement_manager.get_data())

    def get_serialization_data(self, serializer):
        raise NotImplementedError()

    @staticmethod
    def deserialize(serializer):
        raise NotImplementedError()

We overwrite the abstract property requires_stop and the abstract method get_value of the Parameter base class. requires_stop is used to indicate to the Sequencer whether the Parameter object can currently be evaluated or whether the sequencing process has to be interrupted. Our MeasurementDependentParameter will return True if no measurement data is available (in contrast, the ConstantParameter - which internally represents any float value passed in - always returns False). The get_value method returns the parameter value. It is only called if requires_stop is false. In the MesaurementDependentParameter class, we assume that the measured data is a single float and that we simple want to multiply it by 2 as the parameter’s value. The other two methods, get_serialization_data and deserialize also must be overwritten since each Parameter implements the `Serializable interface <03Serialization.ipynb>`__. However, we just raise an exception here since these methods are not relevant in this example.

We would then set up our pulse for execution like as in the following snippet (including a stub implementation of a MeasurementManager just for demonstration purposes):

In [3]:
from qctoolkit.pulses import Sequencer

# We define a stub for the measurement manager here only for illustration purposes.
class MeasurementManager:
    def __init__(self, sequencer: Sequencer) -> None:
        self.sequencer = sequencer
        self.is_available = False

    def is_measurement_available(self) -> bool:
        return self.is_available

    def get_data(self) -> float:
        return 3

sequencer = Sequencer()
measurement_manager = MeasurementManager(sequencer)
parameter = MeasurementDependentParameter(measurement_manager)

sequencer.push(init_template)
sequencer.push(dependent_template, {'v': parameter})
sequencer.push(measurement_template)
sequencer.push(init_template)

The MeasurementManager.is_measurement_available stub will simply return the value to which we have set the is_available member variable of the class.

When we invoke Sequencer.build, for each template on the sequencing stack it first queries whether or not all parameters can be evaluated. If any of them returns True via the requires_stop method, the sequencing process will be interrupted. In our example, Sequencer will first proceed through the first two subtemplates of sequence_template. When it arrives at dependent_template, it will stop:

In [4]:
first_sequence = sequencer.build()
print(first_sequence)
[<qctoolkit.pulses.instructions.EXECInstruction object at 0x00000000074D5E48>, <qctoolkit.pulses.instructions.EXECInstruction object at 0x00000000074D58D0>, <qctoolkit.pulses.instructions.STOPInstruction object at 0x00000000074D5908>]

As you can see in the output above, only two executions instructions are generated, one for each TablePulseTemplate instance before the dependent_template. Let us now switch the is_available variable of the MeasurementManager instance to True, simulating that we’ve obtained some measurement result, and invoke the Sequencer again:

In [5]:
measurement_manager.is_available = True
second_sequence = sequencer.build()
print(second_sequence)
[<qctoolkit.pulses.instructions.EXECInstruction object at 0x00000000074D5E48>, <qctoolkit.pulses.instructions.EXECInstruction object at 0x00000000074D58D0>, <qctoolkit.pulses.instructions.EXECInstruction object at 0x00000000074B79E8>, <qctoolkit.pulses.instructions.EXECInstruction object at 0x00000000074A8EF0>, <qctoolkit.pulses.instructions.STOPInstruction object at 0x000000000752C2E8>]

We have now obtained the complete sequence with one execution instruction for each TablePulseTemplate. Attention/Broken: Currently this is incorrect behavior: We would want to only get the remainder of the pulse in the second call since we wouldn’t want to execute the first part of the pulse again. Needs fixing (`issue 105 <https://github.com/qutech/qc-toolkit/issues/105>`__).

Conditional Execution

The qctoolkit is desinged to support conditional execution of pulse (segments). This allows pulse designers to play back different waveforms depending on, e.g., environment data or state measurements of the quantum dot. Conditional execution may be implemented via trigger-based jumps to instructions directly on the playback device or emulated in software by choosing which pulse to send to the hardware for playback, if the hardware itself does not support branching.

Since the decision whether a condition will be evaluated software- or hardware-based depends on the hardware setup of the experiment and not on the pulse template the qctoolkit relies on an indirection scheme for conditions which is similar to the handling of parameters, as you will see in the following.

qctoolkit offers two PulseTemplate subclasses for conditional execution: LoopPulseTemplate and BranchPulseTemplate (Note: BranchPulseTemplate is currently pending implementation (`issue 22 <https://github.com/qutech/qc-toolkit/issues/22>`__)).

LoopPulseTemplate takes an identifier for a condition and a subtemplate which is repeated as long as the condition evaluates to True. Let’s assume that we want to construct a pulse that waits until some initialization is completed using a TablePulseTemplate representing a zero-pulse of length 5 and a LoopPulseTemplate:

In [4]:
from qctoolkit.pulses import LoopPulseTemplate, TablePulseTemplate

wait_template = TablePulseTemplate()
wait_template.add_entry(5, 0)

loop_template = LoopPulseTemplate('initialization_in_progress', wait_template)

loop_template is now configured to evaluate a condition called ‘initialization_in_progress’. How this condition is implemented needs not to be specified to declare pulse templates.

We will now look into the actual implementation of conditions. The abstract interface of conditions is the Condition class. As mentioned in the beginning, conditions can be evaluated software- and hardware-based. The classes SoftwareCondition and HardwareCondition represent this in the qctoolkit. Note that these classes don’t do the actual evaluation but encapsulate it against the Sequencer and are used to generate correct sequences for both cases. Instances of Conditions are passed directly into the Sequencer and mapped to the PulseTemplates via the identifier, similar to parameters.

Software-Based Conditions

SoftwareCondition takes a callback function which is called to evaluate the condition (and thus must return a boolean value). During the sequencing process, this function will be called. If the return value is True, the subtemplate will be included in the instruction sequence and another evaluation will be made until the return value is False. The generated instruction sequence will thus contain a number of repetitions of the subtemplate but no actual jumping instructions, the loop is essentially unrolled. The callback function is passed an integer value indicating the current iteration of the evaluation.

As an example, we could use this to repeat the wait_template a fixed number of times, say 5, as follows:

In [6]:
from qctoolkit.pulses import SoftwareCondition, Sequencer

constant_repeat_condition = SoftwareCondition(lambda x: x < 5)
conditions = {'initialization_in_progress': constant_repeat_condition}

s = Sequencer()
parameters = {}
s.push(loop_template, parameters, conditions)
instructions = s.build()
print(instructions)
[<qctoolkit.pulses.instructions.EXECInstruction object at 0x000000000748DAC8>, <qctoolkit.pulses.instructions.EXECInstruction object at 0x000000000748DC18>, <qctoolkit.pulses.instructions.EXECInstruction object at 0x000000000748DC88>, <qctoolkit.pulses.instructions.EXECInstruction object at 0x000000000748DCF8>, <qctoolkit.pulses.instructions.EXECInstruction object at 0x000000000748DD68>, <qctoolkit.pulses.instructions.STOPInstruction object at 0x000000000748DDA0>]

We obtain an instruction sequence that repeats an execution instruction (for the wait_template waveform) five times. This is, of course, a very simple example. The callback function passed into the SoftwareCondition instance will more likely evaluate some measured data. Since this might not always be available in the sequencing pass, the callback may return None, which will interrupt the sequencing process similar to the `requires_stop method of the Parameter class <05Parameters.ipynb>`__.

Hardware-Based Conditions

Since software-based evaluation of conditions is slow and might interrupt the sequencing process, it might not always be applicable. If supported by the hardware, hardware-based condition evaluation is preferrable. For the sequencing process, this is represented by instances of HardwareCondition, which only take some identifier of the hardware trigger. This must first be obtained from the hardware currently not implemented and is embedded in the generated instruction sequence which will contain jump instructions.

Assuming we have a hardware setup with a trigger that fires continuously until temperature threshold is reached and we want our pulse from above to repeat as long as the trigger fires. We can realize this as follows:

In [8]:
from qctoolkit.pulses import HardwareCondition, Trigger

# stub representation for the trigger which must be obtained from the hardware in a real use case
temperature_trigger = Trigger()

temperature_trigger_condition = HardwareCondition(temperature_trigger)
conditions = {'initialization_in_progress': temperature_trigger_condition}

s = Sequencer()
parameters = {}
s.push(loop_template, parameters, conditions)
instructions = s.build()
print(instructions)
[<qctoolkit.pulses.instructions.CJMPInstruction object at 0x000000000748DF28>, <qctoolkit.pulses.instructions.STOPInstruction object at 0x000000000748DFD0>, <qctoolkit.pulses.instructions.EXECInstruction object at 0x0000000007462DD8>, <qctoolkit.pulses.instructions.GOTOInstruction object at 0x000000000749B080>]

As you can see in the output, the sequencing process now procudes instructions that perform conditional jumps and returns with goto instructions. The details are omitted here, suffice it to say that the trigger is embedded in the conditional jump instruction and this sequence will loop on the hardware as long as the trigger fires.

In [ ]:

Pulse-Control Integration

In [ ]:

Gate Configuration - A Real Use Case

An example for a real use case of the qctoolkit is the search for and evaluation of parameters for pulses that represent quantum gate operations.

Description of the Experiment

The experiment will typically involve a set of gate pulses \(G_j, 0 \leq j \lt N_{Gates}\).

The template for a gate pulse \(G_j\) is a sequence of \(\epsilon_i, 0 \leq i \lt N_{G_j}\) voltage levels held for time \(\Delta t = 1\) ns as illustrated in the figure below (with \(N_{G_j} = 7\)).

Template of a gate pulse

Template of a gate pulse

The experiment defines a number of sequences \(S_k, 0 \leq k \lt N_{Sequences}\) of the \(G_j\) as

\[S_k = (G_{m_k(1)}, G_{m_k(2)}, \dots, G_{m_k(N_{S_k})})\]

where \(N_{S_k}\) is the length of sequence \(k\) and \(m_k(i): \{0, \dots, N_{S_k} - 1\} \rightarrow \{0, \dots, N_{Gates} - 1\}\) is a function that maps an index \(i\) to the \(m_k(i)\)-th gate of sequence \(S_k\) and thus fully describes the sequence. (These sequences express the sequential application of the gates to the qubit. In terms of quantum mathematics they may rather be expressed as multiplication of the matrices describing the unitary transformations applied by the gates: \(S_k = \prod_{i=N_{S_k} - 1}^{0} G_{m_k(i)} = G_{(N_{S_k} - 1)} \cdot \dots \cdot G_{1} \cdot G_{0}\).)

Measuring and analysing the effects of these sequences on the qubit’s state to derive parameters \(\epsilon_i\) for gate pulses that achieve certain state transformations is the goal of the experiment.

To this end, every sequence must be extended by some preceeding initialization pulse and a succeeding measurement pulse. Furthermore, due to hardware constraints in measuring, all sequences must be of equal length (which is typically 4 µs). Thus, some sequences require some wait time before initialization to increase their playback duration. These requirements give raise to extended sequences \(S_k'\) of the form:

\[S_k' = I_{p(k)} | W_k | S_k | M_{q(k)}\]

where the functions \(p(k)\) and \(q(k)\) respectively select some initialization pulse \(I_{p(k)} \in \{I_1, I_2, \dots\}\) and measurement pulse \(M_{q(k)} \in \{M_1, M_2, \dots\}\) for sequence \(k\) and \(W_k\) is the aforementioned wait pulse. The ‘|’ denote concatenation of pulses, i.e., sequential execution.

Since measurement of quantum state is a probabilistic process, many measurements of the effect of a single sequence must be made to reconstruct the resulting state of the qubit. Thus, the experiment at last defines scanlines (typically of duration 1 second), which are sequences of the \(S_k'\). (These simply represent batches of sequences to configure playback and measurement systems and have no meaning to the experiment beyond these practical considerations.)

Implementation Using the qctoolkit

We now want to illustrate how to setup the experiment described above using the qctoolkit. Let us assume the experiment considers only two different gates pulses (\(N_{Gates} = 2\)). We further assume that \(N_{G_1} = 20\) and \(N_{G_2} = 18\). We define them using instances of TablePulseTemplate:

In [2]:
from qctoolkit.pulses import TablePulseTemplate

delta_t = 1 # assuming that delta_t is 1 elementary time unit long in pulse defintions, i.e. 1 time unit = 1 ns

gate_0 = TablePulseTemplate()
for i in range(0, 19):
    gate_0.add_entry((i) * delta_t, 'gate_0_eps_' + str(i))

gate_1 = TablePulseTemplate()
for i in range(0, 17):
    gate_1.add_entry((i) * delta_t, 'gate_1_eps_' + str(i))

gates = [gate_0, gate_1]

We thus obtain two TablePulseTemplate of the desired form with parameters ‘gate_1_eps_1’ to ‘gate_1_eps_20’ and ‘gate_2_eps_1’ to ‘gate_2_eps_18’.

Next, we will define sequences as SequncePulseTemplate objects. We assume that the mapping functions \(m_k\) are given as a 2 dimensional array (which impilicitly also defines \(N_{Sequences}\) and \(N_{S_k}\)), e.g.:

In [3]:
m = [
      [0, 1, 0, 0, 0, 1, 1, 0, 1], # m_0(i)
      [1, 1, 0, 0, 1, 0], # m_1(i)
      [1, 0, 0, 1, 1, 0, 0, 1] #m_2(i)
    ]

The SequencePulseTemplate objects can now easily be constructed:

In [4]:
from qctoolkit.pulses import SequencePulseTemplate

# SequencePulseTemplate requires a definition of parameter mappings from parameters passed into the
# SequencePulseTemplate object to its subtemplates. In this case, we want parameters to map 1 to 1
# and thus create an identity mapping of parameter names for both gates using python list/set/dict comprehension
epsilon_mappings = [
    {param_name: param_name for param_name in gates[0].parameter_names},
    {param_name: param_name for param_name in gates[1].parameter_names}
]
all_epsilons = gates[0].parameter_names | gates[1].parameter_names

sequences = []
for m_k in m:
    subtemplates = []
    for g_ki in m_k:
        subtemplates.append((gates[g_ki], epsilon_mappings[g_ki]))
    sequences.append(SequencePulseTemplate(subtemplates, all_epsilons))

We end up with a list sequences which contains the sequences described by our \(m\) as qctoolkit pulse templates.

To visualize our progress, let us plot our two gates and the second sequence with some random values between \(-5\) and \(5\) for the \(\epsilon_i\):

In [17]:
import random

random.seed('Some seed such that numbers generated are predictable')
parameters = {parameter_name: random.random() * 10 - 5 for parameter_name in all_epsilons}

%matplotlib inline
from qctoolkit.pulses import plot
plot(gates[0], parameters)
plot(gates[1], parameters)
plot(sequences[1], parameters)
C:\Anaconda3\lib\site-packages\matplotlib\figure.py:387: UserWarning: matplotlib is currently using a non-GUI backend, so cannot show the figure
  "matplotlib is currently using a non-GUI backend, "
_images/examples_08RealWorldCase_9_1.png
_images/examples_08RealWorldCase_9_2.png
_images/examples_08RealWorldCase_9_3.png

We now must construct the \(S_k'\). For simplicity, we assume that there is only one initialization and one measurement pulse which are defined somehow and define only stubs here. We also define a waiting pulse with a variable length:

In [6]:
# stub for an initialization pulse of length 4
init = TablePulseTemplate()
init.add_entry(0, 5)
init.add_entry(4, 0, 'linear')

# stub for a measurement pulse of length 12
measure = TablePulseTemplate()
measure.add_entry(0, 0)
measure.add_entry(12, 5, 'linear')

# a wating pulse
wait = TablePulseTemplate()
wait.add_entry('wait_duration', 0)

For our example, let us assume that we want all \(S_k'\) to take 200 ns (since we’ve chosen the \(S_k\) to be rather short). We know that the duration of our gate pulses in nanoseconds is equal to the number of entries in the TablePulseTemplate objects (each voltage level is held for one unit of time in the tables which corresponds to \(\Delta t = 1\) ns). Accordingly, the init pulse lasts for 4 ns and the measure pulse for 12 ns. The required length of the wait pulse can then be computed as follows:

In [7]:
wait_times = []
desired_time = 200

for m_k in m:
    duration_k = 4 + 12 # init + measurement duration
    for g_ki in m_k:
        duration_k += len(gates[g_ki].entries) # add the number of entries of all gates in the sequence
    wait_time_k = desired_time - duration_k
    wait_times.append(wait_time_k)

print(wait_times)
[21, 76, 40]

Finally we can construct the \(S_k'\):

In [8]:
# an identity mapping for all epsilons
all_epsilons_map = {param_name: param_name for param_name in all_epsilons}

final_sequences = []
for k in range(0, len(sequences)):

    #prepare the subtemplate of the sequence S_k'
    subtemplates = []

    # include the wait pulse first. pass in the appropriate wait time.
    # note that the wait time has to be cast to a string. In parameter mappings, some string containing a mathematical
    # expression is expected. Here, we provide a mathematical expression consisting only of a constant value.
    subtemplates.append((wait, {'wait_duration': str(wait_times[k])}))

    # append the init pulse
    subtemplates.append((init, {}))

    # append the k-th sequence
    subtemplates.append((sequences[k], all_epsilons_map))

    # append the measuring
    subtemplates.append((measure, {}))

    # construct the object for S_k'
    s_k_prime = SequencePulseTemplate(subtemplates, all_epsilons)
    final_sequences.append(s_k_prime)

Let us plot \(S_1'\) to see what whether we’ve accomplished our goal:

In [9]:
plot(final_sequences[1], parameters)
C:\Anaconda3\lib\site-packages\matplotlib\figure.py:387: UserWarning: matplotlib is currently using a non-GUI backend, so cannot show the figure
  "matplotlib is currently using a non-GUI backend, "
_images/examples_08RealWorldCase_17_1.png

Finally, we construct a single scanline which just repeats all three sequences over and over again. Since our \(S_k'\) are short, we just build a scanline with a duration of 0.6 microseconds. With a duration for each \(S_k'\) of 200 ns, we can fit 1‘000 repetitions of \(S_1' | S_2' | S_3'\) in our scanline.

In [10]:
from qctoolkit.pulses import Sequencer

subtemplates = []
for i in range(0, 1000):
    subtemplates.append((final_sequences[0], all_epsilons_map))
    subtemplates.append((final_sequences[1], all_epsilons_map))
    subtemplates.append((final_sequences[2], all_epsilons_map))

scanline = SequencePulseTemplate(subtemplates, all_epsilons)
In [ ]:

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.

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 0x00000000070423C8>, {'foo': <ConstantParameter 2>}, {'rcon': <qctoolkit.pulses.conditions.SoftwareCondition object at 0x00000000045D1E48>})]

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 0x00000000070423C8>, {'foo': <ConstantParameter 2>}, {'rcon': <qctoolkit.pulses.conditions.SoftwareCondition object at 0x00000000045D1E48>}), (<qctoolkit.pulses.table_pulse_template.TablePulseTemplate object at 0x00000000045D1B00>, {'foo': <ConstantParameter 2>}, {'rcon': <qctoolkit.pulses.conditions.SoftwareCondition object at 0x00000000045D1E48>})]
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 0x00000000070424A8>]

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 0x00000000070423C8>, {'foo': <ConstantParameter 2>}, {'rcon': <qctoolkit.pulses.conditions.SoftwareCondition object at 0x00000000045D1E48>})]

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 0x00000000070423C8>, {'foo': <ConstantParameter 2>}, {'rcon': <qctoolkit.pulses.conditions.SoftwareCondition object at 0x00000000045D1E48>}), (<qctoolkit.pulses.table_pulse_template.TablePulseTemplate object at 0x00000000045D1B00>, {'foo': <ConstantParameter 2>}, {'rcon': <qctoolkit.pulses.conditions.SoftwareCondition object at 0x00000000045D1E48>})]

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 0x00000000070423C8>, {'foo': <ConstantParameter 2>}, {'rcon': <qctoolkit.pulses.conditions.SoftwareCondition object at 0x00000000045D1E48>})]

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 0x00000000070424A8>, <qctoolkit.pulses.instructions.EXECInstruction object at 0x00000000074B5D68>]

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 0x00000000070424A8>, <qctoolkit.pulses.instructions.EXECInstruction object at 0x00000000074B5D68>, <qctoolkit.pulses.instructions.STOPInstruction object at 0x00000000074C0D68>]

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 0x00000000074C94A8>, <qctoolkit.pulses.instructions.EXECInstruction object at 0x00000000074C9588>, <qctoolkit.pulses.instructions.STOPInstruction object at 0x00000000074C95C0>]

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 0x00000000074B55C0>, {'foo': <ConstantParameter 2>}, {'bcon': <qctoolkit.pulses.conditions.HardwareCondition object at 0x00000000074B5400>, 'lcon': <qctoolkit.pulses.conditions.HardwareCondition object at 0x00000000074B5CF8>})]
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 0x00000000074C07F0>]
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 0x00000000074B55F8>, {'foo': <ConstantParameter 2>}, {'bcon': <qctoolkit.pulses.conditions.HardwareCondition object at 0x00000000074B5400>, 'lcon': <qctoolkit.pulses.conditions.HardwareCondition object at 0x00000000074B5CF8>})]
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 0x00000000074C9DA0>, <qctoolkit.pulses.instructions.GOTOInstruction object at 0x00000000074C9D30>]
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 0x00000000074C07F0>, <qctoolkit.pulses.instructions.STOPInstruction object at 0x00000000074D52E8>, <qctoolkit.pulses.instructions.CJMPInstruction object at 0x00000000074C9DA0>, <qctoolkit.pulses.instructions.GOTOInstruction object at 0x00000000074C9D30>, <qctoolkit.pulses.instructions.GOTOInstruction object at 0x00000000074D5390>, <qctoolkit.pulses.instructions.EXECInstruction object at 0x00000000074AAE48>, <qctoolkit.pulses.instructions.GOTOInstruction object at 0x00000000074D5438>, <qctoolkit.pulses.instructions.EXECInstruction object at 0x00000000074AAB00>, <qctoolkit.pulses.instructions.GOTOInstruction object at 0x00000000074D54A8>]
Interconnected instruction block

Interconnected instruction block

This instruction sequence indeed represents our desired behavior.

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

qctoolkit package

Subpackages

qctoolkit.hardware package

Subpackages
Module contents

qctoolkit.pulses package

Submodules
qctoolkit.pulses.branch_pulse_template module
qctoolkit.pulses.conditions module
class qctoolkit.pulses.conditions.Condition(*args, **kwargs) → None[source]

Bases: object

A condition on which the execution of a pulse may depend.

Conditions are used for branching and looping of pulses and thus relevant for BranchPulseTemplate and LoopPulseTemplate. Implementations of Condition may rely on software variables, measured data or be mere placeholders for hardware triggers.

build_sequence_branch(delegator: qctoolkit.pulses.sequencing.SequencingElement, if_branch: qctoolkit.pulses.sequencing.SequencingElement, else_branch: qctoolkit.pulses.sequencing.SequencingElement, sequencer: qctoolkit.pulses.sequencing.Sequencer, parameters: typing.Dict[str, qctoolkit.pulses.parameters.Parameter], conditions: typing.Dict[str, typing.Condition], instruction_block: qctoolkit.pulses.instructions.InstructionBlock) → None[source]

Translate a branching SequencingElement using this Condition into an instruction sequence for the given instruction block using sequencer and the given parameter sets.

delegator refers to the SequencingElement which has delegated the invocation of build_sequence to this Condition object. if_branch and else_branch are the elements to be translated into if and else branch instructions. See also SequencingElement.build_sequence().

build_sequence_loop(delegator: qctoolkit.pulses.sequencing.SequencingElement, body: qctoolkit.pulses.sequencing.SequencingElement, sequencer: qctoolkit.pulses.sequencing.Sequencer, parameters: typing.Dict[str, qctoolkit.pulses.parameters.Parameter], conditions: typing.Dict[str, typing.Condition], instruction_block: qctoolkit.pulses.instructions.InstructionBlock) → None[source]

Translate a looping SequencingElement using this Condition into an instruction sequence for the given instruction block using sequencer and the given parameter sets.

delegator refers to the SequencingElement which has delegated the invocation of build_sequence to this Condition object. body is the loop body element. See also SequencingElement.build_sequence().

requires_stop() → bool[source]

Return True if evaluating this Condition is not possible in the current translation process.

exception qctoolkit.pulses.conditions.ConditionEvaluationException[source]

Bases: Exception

Indicates that a SoftwareCondition cannot be evaluated yet.

class qctoolkit.pulses.conditions.SoftwareCondition(evaluation_callback: typing.Callable[[int], typing.Union[bool, NoneType]]) → None[source]

Bases: qctoolkit.pulses.conditions.Condition

A condition that will be evaluated in the software.

SoftwareConditions are evaluated in software, allowing them to rely on sophisticated measurement evaluation or to be used when the hardware device does not support trigger based jumping instructions.

On the downside, this means that a translation processes may be interrupted because a SoftwareCondition relying on measurement data cannot be evaluated before that data is acquired. In this case, the already translated part has to be executed, the measurement is made and in a subsequent translation, the SoftwareCondition is evaluated and the corresponding instructions of one branch/the loop body are generated without jumping instructions.

This interruption of pulse execution might not be feasible in some environments.

build_sequence_branch(delegator: qctoolkit.pulses.sequencing.SequencingElement, if_branch: qctoolkit.pulses.sequencing.SequencingElement, else_branch: qctoolkit.pulses.sequencing.SequencingElement, sequencer: qctoolkit.pulses.sequencing.Sequencer, parameters: typing.Dict[str, qctoolkit.pulses.parameters.Parameter], conditions: typing.Dict[str, typing.Condition], instruction_block: qctoolkit.pulses.instructions.InstructionBlock) → None[source]
build_sequence_loop(delegator: qctoolkit.pulses.sequencing.SequencingElement, body: qctoolkit.pulses.sequencing.SequencingElement, sequencer: qctoolkit.pulses.sequencing.Sequencer, parameters: typing.Dict[str, qctoolkit.pulses.parameters.Parameter], conditions: typing.Dict[str, typing.Condition], instruction_block: qctoolkit.pulses.instructions.InstructionBlock) → None[source]
requires_stop() → bool[source]
class qctoolkit.pulses.conditions.HardwareCondition(trigger: qctoolkit.pulses.instructions.Trigger) → None[source]

Bases: qctoolkit.pulses.conditions.Condition

A condition that will be evaluated using hardware triggers.

During the translation process, HardwareCondition instanced will produce in code blocks for branches/loop bodies and the corresponding conditional jump instructions.

build_sequence_branch(delegator: qctoolkit.pulses.sequencing.SequencingElement, if_branch: qctoolkit.pulses.sequencing.SequencingElement, else_branch: qctoolkit.pulses.sequencing.SequencingElement, sequencer: qctoolkit.pulses.sequencing.Sequencer, parameters: typing.Dict[str, qctoolkit.pulses.parameters.Parameter], conditions: typing.Dict[str, typing.Condition], instruction_block: qctoolkit.pulses.instructions.InstructionBlock) → None[source]
build_sequence_loop(delegator: qctoolkit.pulses.sequencing.SequencingElement, body: qctoolkit.pulses.sequencing.SequencingElement, sequencer: qctoolkit.pulses.sequencing.Sequencer, parameters: typing.Dict[str, qctoolkit.pulses.parameters.Parameter], conditions: typing.Dict[str, typing.Condition], instruction_block: qctoolkit.pulses.instructions.InstructionBlock) → None[source]
requires_stop() → bool[source]
qctoolkit.pulses.function_pulse_template module
class qctoolkit.pulses.function_pulse_template.FunctionPulseTemplate(expression: str, duration_expression: str, measurement: bool = False, identifier: str = None) → None[source]

Bases: qctoolkit.pulses.pulse_template.PulseTemplate

Defines a pulse via a time-domain expression.

FunctionPulseTemplate stores the expression and its external parameters. The user must provide two things: one expression that calculates the length of the pulse from the external parameters and the time-domain pulse shape itself as a expression. The external parameters are derived from the expressions themselves. Like other PulseTemplates the FunctionPulseTemplate can be declared to be a measurement pulse.

The independent variable in the expression is called ‘t’ and is given in units of nano-seconds.

build_sequence(sequencer: qctoolkit.pulses.sequencing.Sequencer, parameters: typing.Dict[str, qctoolkit.pulses.parameters.Parameter], conditions: typing.Dict[str, typing.Condition], instruction_block: qctoolkit.pulses.instructions.InstructionBlock) → None[source]
static deserialize()[source]
get_measurement_windows(parameters: typing.Union[typing.Dict[str, qctoolkit.pulses.parameters.Parameter], NoneType] = {}) → typing.List[typing.Tuple[float, float]][source]

Return all measurement windows defined in this PulseTemplate.

A ExpressionPulseTemplate specifies either no measurement windows or exactly one that spans its entire duration, depending on whether the measurement_pulse flag was given during construction.

get_pulse_length(parameters: typing.Dict[str, qctoolkit.pulses.parameters.Parameter]) → float[source]

Return the length of this pulse for the given parameters.

get_serialization_data(serializer: qctoolkit.serialization.Serializer) → None[source]
is_interruptable

Return true, if this PulseTemplate contains points at which it can halt if interrupted.

parameter_declarations

Return a set of all parameter declaration objects of this TablePulseTemplate.

parameter_names

Return the set of names of declared parameters.

requires_stop(parameters: typing.Dict[str, qctoolkit.pulses.parameters.Parameter], conditions: typing.Dict[str, typing.Condition]) → bool[source]
class qctoolkit.pulses.function_pulse_template.FunctionWaveform(parameters: typing.Dict[str, float], expression: qctoolkit.expressions.Expression, duration_expression: qctoolkit.expressions.Expression) → None[source]

Bases: qctoolkit.pulses.instructions.Waveform

duration
sample(sample_times: numpy.ndarray, first_offset: float = 0) → numpy.ndarray[source]
qctoolkit.pulses.instructions module
qctoolkit.pulses.instructions.WaveformTable

alias of Tuple

class qctoolkit.pulses.instructions.WaveformTableEntry(t, v, interp)

Bases: tuple

interp

Alias for field number 2

t

Alias for field number 0

v

Alias for field number 1

class qctoolkit.pulses.instructions.Waveform[source]

Bases: qctoolkit.comparable.Comparable

duration

Return the duration of the waveform in time units.

sample(sample_times: numpy.ndarray, first_offset: float = 0) → numpy.ndarray[source]

Sample the waveform.

Will be sampled at the given sample_times. These, however, will be normalized such that the lie in the range [0, waveform.duration] for interpolation. first_offset is the offset of the discrete first sample from the actual beginning of the waveform in a continuous time domain.

class qctoolkit.pulses.instructions.Trigger → None[source]

Bases: qctoolkit.comparable.Comparable

class qctoolkit.pulses.instructions.InstructionPointer(block: qctoolkit.pulses.instructions.InstructionBlock, offset: int) → None[source]

Bases: object

get_absolute_address() → int[source]
class qctoolkit.pulses.instructions.Instruction → None[source]

Bases: qctoolkit.comparable.Comparable

class qctoolkit.pulses.instructions.CJMPInstruction(trigger: qctoolkit.pulses.instructions.Trigger, block: qctoolkit.pulses.instructions.InstructionBlock, offset: int = 0) → None[source]

Bases: qctoolkit.pulses.instructions.Instruction

class qctoolkit.pulses.instructions.EXECInstruction(waveform: qctoolkit.pulses.instructions.Waveform) → None[source]

Bases: qctoolkit.pulses.instructions.Instruction

class qctoolkit.pulses.instructions.GOTOInstruction(block: qctoolkit.pulses.instructions.InstructionBlock, offset: int = 0) → None[source]

Bases: qctoolkit.pulses.instructions.Instruction

class qctoolkit.pulses.instructions.STOPInstruction → None[source]

Bases: qctoolkit.pulses.instructions.Instruction

class qctoolkit.pulses.instructions.InstructionBlock(outer_block: typing.Union[qctoolkit.pulses.instructions.InstructionBlock, NoneType] = None) → None[source]

Bases: object

add_instruction(instruction: qctoolkit.pulses.instructions.Instruction) → None[source]
add_instruction_cjmp(trigger: qctoolkit.pulses.instructions.Trigger, target_block: qctoolkit.pulses.instructions.InstructionBlock, offset: int = 0) → None[source]
add_instruction_exec(waveform: qctoolkit.pulses.instructions.Waveform) → None[source]
add_instruction_goto(target_block: qctoolkit.pulses.instructions.InstructionBlock, offset: int = 0) → None[source]
add_instruction_stop() → None[source]
compile_sequence() → typing.List[qctoolkit.pulses.instructions.Instruction][source]
create_embedded_block() → qctoolkit.pulses.instructions.InstructionBlock[source]
get_start_address() → int[source]
instructions
qctoolkit.pulses.instructions.InstructionSequence

alias of List

exception qctoolkit.pulses.instructions.InstructionBlockNotYetPlacedException[source]

Bases: Exception

Indicates that an attempt was made to obtain the start address of an InstructionBlock that was not yet placed inside the corresponding outer block.

exception qctoolkit.pulses.instructions.InstructionBlockAlreadyFinalizedException[source]

Bases: Exception

Indicates that an attempt was made to change an already finalized InstructionBlock.

exception qctoolkit.pulses.instructions.MissingReturnAddressException[source]

Bases: Exception

Indicates that an inner InstructionBlock has no return address.

qctoolkit.pulses.interpolation module
class qctoolkit.pulses.interpolation.InterpolationStrategy[source]

Bases: object

class qctoolkit.pulses.interpolation.HoldInterpolationStrategy[source]

Bases: qctoolkit.pulses.interpolation.InterpolationStrategy

Holds previous value and jumps to the current value at the last sample.

class qctoolkit.pulses.interpolation.JumpInterpolationStrategy[source]

Bases: qctoolkit.pulses.interpolation.InterpolationStrategy

Jumps to the current value at the first sample and holds.

class qctoolkit.pulses.interpolation.LinearInterpolationStrategy[source]

Bases: qctoolkit.pulses.interpolation.InterpolationStrategy

Interpolates linearly.

qctoolkit.pulses.loop_pulse_template module
class qctoolkit.pulses.loop_pulse_template.LoopPulseTemplate(condition: str, body: qctoolkit.pulses.pulse_template.PulseTemplate, identifier: typing.Union[str, NoneType] = None) → None[source]

Bases: qctoolkit.pulses.pulse_template.PulseTemplate

Conditional looping in a pulse.

A LoopPulseTemplate is a PulseTemplate which is repeated during execution as long as a certain condition holds.

body
build_sequence(sequencer: qctoolkit.pulses.sequencing.Sequencer, parameters: typing.Dict[str, qctoolkit.pulses.parameters.Parameter], conditions: typing.Dict[str, qctoolkit.pulses.conditions.Condition], instruction_block: qctoolkit.pulses.instructions.InstructionBlock) → None[source]
condition
static deserialize(serializer: qctoolkit.serialization.Serializer, condition: str, body: typing.Dict[str, typing.Any], identifier: typing.Union[str, NoneType] = None) → qctoolkit.pulses.loop_pulse_template.LoopPulseTemplate[source]
get_measurement_windows(parameters: typing.Dict[str, qctoolkit.pulses.parameters.Parameter] = None) → typing.Tuple[float, float][source]
get_serialization_data(serializer: qctoolkit.serialization.Serializer) → typing.Dict[str, typing.Any][source]
is_interruptable
parameter_declarations
parameter_names
requires_stop(parameters: typing.Dict[str, qctoolkit.pulses.parameters.Parameter], conditions: typing.Dict[str, qctoolkit.pulses.conditions.Condition]) → bool[source]
exception qctoolkit.pulses.loop_pulse_template.ConditionMissingException(condition_name: str) → None[source]

Bases: Exception

qctoolkit.pulses.parameters module
class qctoolkit.pulses.parameters.Parameter → None[source]

Bases: qctoolkit.serialization.Serializable

A parameter for pulses.

Parameter specifies a concrete value which is inserted instead of the parameter declaration reference in a PulseTemplate if it satisfies the minimum and maximum boundary of the corresponding ParameterDeclaration. Implementations of Parameter may provide a single constant value or obtain values by computation (e.g. from measurement results).

get_value() → float[source]

Compute and return the parameter value.

requires_stop

Return True if the evaluation of this Parameter instance requires a stop in execution/sequencing, e.g., because it depends on data that is only measured in during the next execution.

class qctoolkit.pulses.parameters.ParameterDeclaration(name: str, min: typing.Union[float, typing.ParameterDeclaration] = -inf, max: typing.Union[float, typing.ParameterDeclaration] = inf, default: typing.Union[float, NoneType] = None) → None[source]

Bases: qctoolkit.serialization.Serializable

A declaration of a parameter required by a pulse template.

PulseTemplates may declare parameters to allow for variations of values in an otherwise static pulse structure. ParameterDeclaration represents a declaration of such a parameter and allows for the definition of boundaries and a default value for a parameter.

BoundaryValue

alias of Union

absolute_max_value

Return this ParameterDeclaration’s maximum value.

If the maximum value of this ParameterDeclaration instance is a reference to another instance, references are resolved until a concrete value or None is obtained.

absolute_min_value

Return this ParameterDeclaration’s minimum value.

If the minimum value of this ParameterDeclaration instance is a reference to another instance, references are resolved until a concrete value or None is obtained.

default_value

Return this ParameterDeclaration’s default value.

static deserialize(serializer: qctoolkit.serialization.Serializer, name: str, min_value: typing.Union[str, float], max_value: typing.Union[str, float], default_value: float) → qctoolkit.pulses.parameters.ParameterDeclaration[source]
get_serialization_data(serializer: qctoolkit.serialization.Serializer) → typing.Dict[str, typing.Any][source]
get_value(parameters: typing.Dict[str, qctoolkit.pulses.parameters.Parameter]) → float[source]
is_parameter_valid(p: qctoolkit.pulses.parameters.Parameter) → bool[source]

Checks whether a given parameter satisfies this ParameterDeclaration.

A parameter is valid if all of the following statements hold: - If the declaration specifies a minimum value, the parameter’s value must be greater or equal - If the declaration specifies a maximum value, the parameter’s value must be less or equal

max_value

Return this ParameterDeclaration’s maximum value or reference.

min_value

Return this ParameterDeclaration’s minimum value or reference.

name
class qctoolkit.pulses.parameters.ConstantParameter(value: float) → None[source]

Bases: qctoolkit.pulses.parameters.Parameter

A pulse parameter with a constant value.

static deserialize(serializer: qctoolkit.serialization.Serializer, constant: float) → qctoolkit.pulses.parameters.ConstantParameter[source]
get_serialization_data(serializer: qctoolkit.serialization.Serializer) → None[source]
get_value() → float[source]
requires_stop
exception qctoolkit.pulses.parameters.ParameterNotProvidedException(parameter_name: str) → None[source]

Bases: Exception

Indicates that a required parameter value was not provided.

exception qctoolkit.pulses.parameters.ParameterValueIllegalException(parameter_declaration: qctoolkit.pulses.parameters.ParameterDeclaration, parameter_value: float) → None[source]

Bases: Exception

Indicates that the value provided for a parameter is illegal, i.e., is outside the parameter’s bounds or of wrong type.

qctoolkit.pulses.plotting module
qctoolkit.pulses.pulse_template module
qctoolkit.pulses.pulse_template.MeasurementWindow

alias of Tuple

class qctoolkit.pulses.pulse_template.PulseTemplate(identifier: typing.Union[str, NoneType] = None) → None[source]

Bases: qctoolkit.serialization.Serializable, qctoolkit.pulses.sequencing.SequencingElement

A PulseTemplate represents the parameterized general structure of a pulse.

A PulseTemplate described a pulse in an abstract way: It defines the structure of a pulse but might leave some timings or voltage levels undefined, thus declaring parameters. This allows to reuse a PulseTemplate for several pulses which have the same overall structure and differ only in concrete values for the parameters. Obtaining an actual pulse which can be executed by specifying values for these parameters is called instantiation of the PulseTemplate.

get_measurement_windows(parameters: typing.Dict[str, qctoolkit.pulses.parameters.Parameter] = None) → typing.List[typing.Tuple[float, float]][source]

Return all measurement windows defined in this PulseTemplate.

is_interruptable

Return true, if this PulseTemplate contains points at which it can halt if interrupted.

parameter_declarations

Return the set of ParameterDeclarations.

parameter_names

Return the set of names of declared parameters.

qctoolkit.pulses.repetition_pulse_template module
qctoolkit.pulses.sequence_pulse_template module
class qctoolkit.pulses.sequence_pulse_template.SequencePulseTemplate(subtemplates: typing.List[typing.Tuple[qctoolkit.pulses.pulse_template.PulseTemplate, typing.Dict[str, str]]], external_parameters: typing.List[str], identifier: typing.Union[str, NoneType] = None) → None[source]

Bases: qctoolkit.pulses.pulse_template.PulseTemplate

A sequence of different PulseTemplates.

SequencePulseTemplate allows to group smaller PulseTemplates (subtemplates) into on larger sequence, i.e., when instantiating a pulse from a SequencePulseTemplate all pulses instantiated from the subtemplates are queued for execution right after one another. SequencePulseTemplate allows to specify a mapping of parameter declarations from its subtemplates, enabling renaming and mathematical transformation of parameters. The default behavior is to exhibit the union of parameter declarations of all subtemplates. If two subpulses declare a parameter with the same name, it is mapped to both. If the declarations define different minimal and maximal values, the more restricitive is chosen, if possible. Otherwise, an error is thrown and an explicit mapping must be specified. ^outdated

build_sequence(sequencer: qctoolkit.pulses.sequencing.Sequencer, parameters: typing.Dict[str, qctoolkit.pulses.parameters.Parameter], conditions: typing.Dict[str, qctoolkit.pulses.conditions.Condition], instruction_block: qctoolkit.pulses.instructions.InstructionBlock) → None[source]
static deserialize(serializer: qctoolkit.serialization.Serializer, is_interruptable: bool, subtemplates: typing.Iterable[typing.Dict[str, typing.Union[str, typing.Dict[str, typing.Any]]]], external_parameters: typing.Iterable[str], identifier: typing.Union[str, NoneType] = None) → qctoolkit.pulses.sequence_pulse_template.SequencePulseTemplate[source]
get_measurement_windows(parameters: typing.Dict[str, qctoolkit.pulses.parameters.Parameter] = None) → typing.List[typing.Tuple[float, float]][source]
get_serialization_data(serializer: qctoolkit.serialization.Serializer) → typing.Dict[str, typing.Any][source]
is_interruptable
parameter_declarations
parameter_names
requires_stop(parameters: typing.Dict[str, qctoolkit.pulses.parameters.Parameter], conditions: typing.Dict[str, typing.Condition]) → bool[source]
exception qctoolkit.pulses.sequence_pulse_template.MissingMappingException(template, key) → None[source]

Bases: Exception

exception qctoolkit.pulses.sequence_pulse_template.MissingParameterDeclarationException(template: qctoolkit.pulses.pulse_template.PulseTemplate, missing_delcaration: str) → None[source]

Bases: Exception

exception qctoolkit.pulses.sequence_pulse_template.UnnecessaryMappingException(template: qctoolkit.pulses.pulse_template.PulseTemplate, key: str) → None[source]

Bases: Exception

qctoolkit.pulses.sequencing module
class qctoolkit.pulses.sequencing.SequencingElement → None[source]

Bases: object

An entity which can be sequenced using Sequencer.

build_sequence(sequencer: qctoolkit.pulses.sequencing.Sequencer, parameters: typing.Dict[str, qctoolkit.pulses.parameters.Parameter], conditions: typing.Dict[str, typing.Condition], instruction_block: qctoolkit.pulses.instructions.InstructionBlock) → None[source]

Translate this SequencingElement into an instruction sequence for the given instruction_block using sequencer and the given parameter sets.

Implementation guide: Use instruction_block methods to add instructions or create new InstructionBlocks. Use sequencer to push child elements to the translation stack.

requires_stop(parameters: typing.Dict[str, qctoolkit.pulses.parameters.Parameter], conditions: typing.Dict[str, typing.Condition]) → bool[source]

Return True if this SequencingElement cannot be translated yet.

Sequencer will check requires_stop() before calling build_sequence(). If requires_stop() returns True, Sequencer interrupts the current translation process and will not call build_sequence().

Implementation guide: requires_stop() should only return True, if this SequencingElement cannot be build, i.e., the return value should only depend on the parameters/conditions of this SequencingElement, not on possible child elements. If this SequencingElement contains a child element which requires a stop, this information will be regarded during translation of that element.

class qctoolkit.pulses.sequencing.Sequencer → None[source]

Bases: object

Translates tree structures of SequencingElement objects to linear instruction sequences contained in a InstructionBlock.

StackElement

alias of Tuple

build() → qctoolkit.pulses.instructions.InstructionBlock[source]
Start the translation process. Translate all elements currently on the translation stacks into a sequence
and return the InstructionBlock of the main sequence.

Processes all stacks (for each InstructionBlock) until each stack is either empty or its topmost element requires a stop. If build is called after a previous translation process where some elements required a stop (i.e., has_finished returned False), it will append new modify the previously generated and returned main InstructionBlock. Make sure not to rely on that being unchanged.

has_finished() → bool[source]

Returns True, if all translation stacks are empty. Indicates that the translation is complete.

Note that has_finished that has_finished will return False, if there are stack elements that require a stop. In this case, calling build will only have an effect if these elements no longer require a stop, e.g. when required measurement results have been acquired since the last translation.

push(sequencing_element: qctoolkit.pulses.sequencing.SequencingElement, parameters: typing.Dict[str, typing.Union[qctoolkit.pulses.parameters.Parameter, float]] = {}, conditions: typing.Dict[str, typing.Condition] = {}, target_block: qctoolkit.pulses.instructions.InstructionBlock = None) → None[source]

Add an element to the translation stack of the target_block with the given set of parameters.

The element will be on top of the stack, i.e., it is the first to be translated if no subsequent calls to push with the same target_block occur.

qctoolkit.pulses.table_pulse_template module
Module contents

qctoolkit.qcmatlab package

Submodules
qctoolkit.qcmatlab.manager module
qctoolkit.qcmatlab.pulse_control module
class qctoolkit.qcmatlab.pulse_control.PulseControlInterface(sample_rate: float, time_scaling: float = 0.001) → None[source]

Bases: object

Pulse

alias of Dict

PulseGroup

alias of Dict

create_pulse_group(sequence: typing.List[qctoolkit.pulses.instructions.Instruction], name: str) → typing.Tuple[typing.PulseControlInterface.PulseGroup, typing.List[typing.PulseControlInterface.Pulse]][source]

Construct a dictionary adhering to the pulse group struct definition in pulse control.

All waveforms in the given InstructionSequence are converted to waveform pulse structs and returned as a list in the second component of the returned tuple. The first component of the result is a pulse group dictionary denoting the sequence of waveforms using their indices in the returned list. create_pulse_group detects multiple use of waveforms and sets up the pulse group dictionary accordingly.

Note that pulses are not registered in pulse control. To achieve this and update the pulse group struct accordingly, the dedicated MATLAB script has to be invoked.

The function will raise an Exception if the given InstructionBlock does contain branching instructions, which are not supported by pulse control.

Arguments: sequence – The InstructionSequence to convert. name – Value for the name field in the resulting pulse group dictionary.

create_waveform_struct(waveform: qctoolkit.pulses.instructions.Waveform, name: str) → typing.Dict[str, typing.Any][source]

Construct a dictionary adhering to the waveform struct definition in pulse control.

Arguments: waveform – The Waveform object to convert. name – Value for the name field in the resulting waveform dictionary.

Module contents

qctoolkit.utils package

Submodules
qctoolkit.utils.type_check module

RELATED THIRD PARTY IMPORTS

exception qctoolkit.utils.type_check.MismatchingTypesException(message) → None[source]

Bases: Exception

Exception raised when a type error occurs in “type_check”

Has the behaviour of an default Exception due inheritance of the self.

qctoolkit.utils.type_check.typecheck(same_length: bool = False, raise_on_error: bool = False, log: bool = True)[source]

Decorator for functions, invokes typechecking on their annotation

This function invokes the typechecking on functions which uses the annotations of python3. This type check will happen in runtime.

For the “same_length” argument, please reference to “__equal_types”.

There are two ways, this function can react on type errors: 1. “raise_on_error” will raise an “MismatchingTypeException” defined below. 2. “log” will create a logging message on the warning level.

Usage: “@typecheck: ‘def f(x):”

Module contents

Submodules

qctoolkit.comparable module

class qctoolkit.comparable.Comparable[source]

Bases: object

qctoolkit.expressions module

class qctoolkit.expressions.Expression(ex: str) → None[source]

Bases: qctoolkit.serialization.Serializable

static deserialize()[source]
evaluate(**kwargs) → float[source]
get_serialization_data()[source]
identifier
string
variables() → typing.Iterable[str][source]

qctoolkit.serialization module

class qctoolkit.serialization.StorageBackend[source]

Bases: object

exists(identifier: str) → bool[source]

Return True, if data is stored for the given identifier.

get(identifier: str) → str[source]

Retrieve the data string with the given identifier.

put(identifier: str, data: str, overwrite: bool = False) → None[source]

Store the data string identified by identifier.

class qctoolkit.serialization.FilesystemBackend(root: str = '.') → None[source]

Bases: qctoolkit.serialization.StorageBackend

exists(identifier: str) → bool[source]
get(identifier: str) → str[source]
put(identifier: str, data: str, overwrite: bool = False) → None[source]
class qctoolkit.serialization.CachingBackend(backend: qctoolkit.serialization.StorageBackend) → None[source]

Bases: qctoolkit.serialization.StorageBackend

exists(identifier: str) → bool[source]
get(identifier: str) → str[source]
put(identifier: str, data: str, overwrite: bool = False) → None[source]
class qctoolkit.serialization.Serializable(identifier: typing.Union[str, NoneType] = None) → None[source]

Bases: object

static deserialize(serializer: qctoolkit.serialization.Serializer, **kwargs) → qctoolkit.serialization.Serializable[source]

Reconstruct the Serializable object from a dictionary containing all relevant information as obtained from get_serialization_data.

get_serialization_data(serializer: qctoolkit.serialization.Serializer) → typing.Dict[str, typing.Any][source]

Return all data relevant for serialization as a dictionary containing only base types.

identifier
class qctoolkit.serialization.Serializer(storage_backend: qctoolkit.serialization.StorageBackend) → None[source]

Bases: object

deserialize(representation: typing.Union[str, typing.Dict[str, typing.Any]]) → qctoolkit.serialization.Serializable[source]
dictify(serializable: qctoolkit.serialization.Serializable) → typing.Dict[str, typing.Dict[str, typing.Any]][source]
static get_type_identifier(obj: typing.Any) → str[source]
serialize(serializable: qctoolkit.serialization.Serializable) → None[source]

Module contents

Indices and tables