2.8. Multi-Channel Pulses

Usually there is a need to define pulses for multiple control channels simulateously. While this would be possible by simply defining several separate pulse templates (one for each channel), the qctoolkit also allows to define pulse templates directly for multiple channels or combine existing templates in a multi-channel way. This tutorial explores these possibilities.

2.8.1. A Multi-Channel Table Pulse

TablePulseTemplate allows to model multiple channel in a straighforward way: We simply initialize our TablePulseTemplate instance with the channels argument. When we then add new entries to it, we specify the channel for the entry with the channel argument of the add_entry method. Note that this is zero-indexed. We may share parameters between the channels. The following example constructs a 2-channel table pulse template with shared parameters and plots it.

In [3]:
from qctoolkit.pulses import TablePT

table_template = TablePT(identifier='2-channel-table-template',
                         entries={0: [(0, 0),
                                      (1, 4),
                                      ('foo', 'bar'),
                                      (10, 0)],
                                  1: [(0, 0),
                                      ('foo', 2.7, 'linear'),
                                      (9, 'bar', 'linear')]})

# plot it
%matplotlib notebook
from qctoolkit.pulses.plotting import plot
parameters = dict(
    foo=7,
    bar=-1.3
)
_ = plot(table_template, parameters, sample_rate=100)
print("The number of channels in table_template is {}.".format(table_template.num_channels))
The number of channels in table_template is 2.

2.9. Combining Templates: AtomicMultiChannelPulseTemplate

AtomicMultiChannelPulseTemplate(AtomicMultiChannelPT) allows to compose a multi-channel template out of atomic (i.e., no control flow) templates of equal duration. It allows to reassign channel indices of the channels of its subtemplates. The constructor is similar to the one of SequencePulseTemplate and expects subtemplates including parameter and channel mappings as well as a set of external parameters.

The following example will combine the two-channel table pulse template table_template from above and a function pulse template function_template to a three-channel template template. We reassign indices such that channel ‘chan_B’ of template is channel 0 and ‘chan_B’ is channel 1 of table_template. Furthermore the parameters get remapped. function_template doesnt get changed at all.

In [10]:
from qctoolkit.pulses import FunctionPT, AtomicMultiChannelPT

function_template = FunctionPT('-sin(t)**2', '10', identifier='function-template', channel='chan_A')

template = AtomicMultiChannelPT(
    function_template,
    (table_template, dict(foo='5', bar='2 * hugo'), {0: 'chan_B', 1: 'chan_C'}),
    identifier='3-channel-combined-template'
)

_ = plot(template, dict(hugo=-1.3), sample_rate=100)
print("The number of channels in function_template is {}.".format(function_template.num_channels))
print("The number of channels in template is {}.".format(template.num_channels))
The number of channels in function_template is 1.
The number of channels in template is 3.

The constructor of AtomicMultiChannelPulseTemplate expects its subtemplates as positional arguments. Each of positional arguments is required to be either a AtomicPulseTemplate, a MappingPulseTemplate that wraps an AtomicPulseTemplate or a tuple that can be passed to MappingPulseTemplate.from_tuple(more examples here). There sets of channels where the subtemplates are defined on has to be destinct. Note that an exception will be raised during the sampling of the waveforms (i.e., during the sequencing process) if the subtemplates have different length.

2.9.1. Multiple Channels in Non-Atomic Templates

All higher order template, i.e., SequencePulseTemplate and ForLoopPulseTemplate and also support multiple channels insofar as that they can be composed using multi-channel atomic templates as subtemplates. They require that all these subtemplates define the same channels and raise an exception if that is not the case. The following example constructs a SequencePulseTempate sequence_template by chaining the above defined two-channel table_template. In the second instance of table_template in the sequence, we swap the channels by wrapping a MultiChannelPulseTemplate around it.

In [12]:
from qctoolkit.pulses import SequencePT
from qctoolkit.pulses import MappingPT

sequence_template = SequencePT(
    (table_template, dict(foo='1.2 * hugo', bar='hugo ** 2')),
    (table_template, dict(foo='1.2 * hugo', bar='hugo ** 2'), {0: 1, 1: 0}),
    identifier='2-channel-sequence-template'
)

plot(sequence_template, dict(hugo=2), sample_rate=100)
print("The number of channels in sequence_template is {}.".format(sequence_template.num_channels))
The number of channels in sequence_template is 2.