Basic pulsed tutorial

pulsed mode is used to schedule, output and measure pulses with different lengths, shapes, frequencies and amplitudes. In this tutorial, we go step by step through basic examples of how to use the pulsed mode. You can also find these examples in the presto-demo repository on GitHub.

Tip

To be able to run the following examples while reading through this tutorial, you should connect one output port of Presto to one of its inputs, through the low-pass filter VLFX-2500+. Keep in mind that the analog performance of each channel of Presto is customizable. Make sure to pick a port number that can output low frequencies.

Output and measure one pulse

Using a template

On each output and input, Presto has a number of resources we can access through the Python API presto. With this first example, we output a single pulse in order to learn what resources are available and how to program them. We first need to import a few useful classes and define the parameters of the pulse.

Note

To install the Presto Python API, follow the instructions at Installing and updating.

import matplotlib.pyplot as plt
import numpy as np

from presto import pulsed
from presto.hardware import AdcMode, DacMode
from presto.utils import sin2, plot_sequence

OUTPUT_PORT = 1  # 1 to 16, can be a list
INPUT_PORT = 1  # 1 to 16, can be a list

Then we create the pls object, an instance of the Pulsed class:

with pulsed.Pulsed(
    address="192.168.20.23",
    ext_ref_clk=False,
    adc_mode=AdcMode.Direct,
    dac_mode=DacMode.Direct,
) as pls:

This will connect to the Presto unit at the specified IP address and set up the clocks using internal reference (ext_ref_clk=False). We also tell the API we want to output and measure pulses without using the digital up- and down-conversion, by setting AdcMode and DacMode to Direct. In Direct mode we can output and measure pulses from 0 to 1 GHz.


Next, we set a few Hardware-related parameters, which usually don’t change during the measurement:

    pls.hardware.set_dac_current(OUTPUT_PORT, 40_500)  # μA, 2250 to 40500
    pls.hardware.set_inv_sinc(OUTPUT_PORT, 0)
    pls.hardware.set_adc_attenuation(INPUT_PORT, 0.0)  # dB, 0.0 to 27.0

We set the analog output range for the OUTPUT_PORT to its maximum value, disable the use of the inverse-sinc FIR filter and disable the input attenuation (0 dB). These are optional parameters, but it’s good to know that they exist.


We configure the data acquisition by specifying that we want to store (sample) data from the INPUT_PORT for 2 μs each time:

    pls.setup_store(INPUT_PORT, duration=2e-6)

We then define our output pulse as an arbitrary waveform with duration 200 ns, frequency 42.5 MHz, amplitude 0.5 FS and an envelope with a sine-squared shape (also known as a Hann function):

    amp = 0.5  # FS
    duration = 200e-9  # s
    freq = 42.5e6  # Hz

    ns = int(round(duration * pls.get_fs("dac")))
    t = np.linspace(0, duration, ns, endpoint=False)
    data = amp * sin2(ns) * np.sin(2 * np.pi * freq * t)

    pulse_1 = pls.setup_template(OUTPUT_PORT, group=0, template=data)

Here, ns is the number of samples in an output pulse with the specified duration. With this number, we define a time array t and the envelope shape by using the helper function utils.sin2(). We create the NumPy array data with the raw data points of the waveform, and use setup_template() to create the output pulse pulse_1 on output port 1 and group 0.

Each output port has enough memory for 16 templates split into two groups: 8 templates in group 0 and 8 templates in group 1. Templates in the same group share a programmable carrier generator and amplitude scaler.

Tip

You can find the functional schematics of the pulsed output at Pulsed output.


We setup the scale look-up table (LUT) on output port 1 and group 0 to contain only one value. We just set 1.0, because the amplitude of the pulse is already encoded in the data points of pulse_1.

    pls.setup_scale_lut(OUTPUT_PORT, group=0, scales=1.0)

Now that all the resources are set up, we proceed to scheduling the pulse sequence:

    T = 0.0
    pls.store(T)
    pls.output_pulse(T, pulse_1)

In this case it is a very simple sequence. At time T=0 we output a pulse, and at the same time we open a sampling window.


We could now just run the experiment and look at the data, but let’s first use utils.plot_sequence() to visualize the sequence we programmed. This is especially useful when sequences become complex and is in general a helpful troubleshooting method.

    plot_sequence(pls, period=4e-6, repeat_count=1, num_averages=1)
../../_images/pulsed_demo_1_sequence_light.svg ../../_images/pulsed_demo_1_sequence_dark.svg

In our case, we see that we programmed a pulse to be output at T=0 on channel 1 group 0. At the same time, the sampling window starts.

Tip

If you don’t have access to a Presto unit right now, you can still run this part of the code. Just pass dry_run=True when initializing the pls instance of the Pulsed class.


It’s now time to run() the pulse sequence!

    pls.run(period=4e-6, repeat_count=1, num_averages=1)
    t_arr, data = pls.get_store_data()

Here, period is the duration of the pulse sequence. repeat_count allows us to repeat this sequence a certain number of times, and potentially sweep one or multiple parameters while doing so. For now we run the sequence only once. num_averages lets us repeat the sequence and average the sampled data. In this example we do not average.

We request the sampled data with get_store_data() and, finally, we can plot the result.

fig, ax = plt.subplots(tight_layout=True, figsize=(6, 2.5))
ax.plot(1e9 * t_arr, data[0, 0, :])
ax.set_xlabel("Time [ns]")
ax.set_ylabel("Input signal [FS]")
plt.show()
../../_images/pulsed_demo_1a_light.svg ../../_images/pulsed_demo_1a_dark.svg

We measure the pulse as expected. The delay of about 200 ns includes the latency of DAC and ADC converters in Presto, as well as the small delay introduced by the cables and low-pass filter. This delay will be constant as long as the cabling is not changed.

Using scale look-up tables

Sometimes it is beneficial to encode the amplitude of the pulse in the scale look-up table (LUT). For example, we might want to output pulses with the same shape but different amplitude one after another. Let’s modify the code to output two pulses back-to-back with amplitudes amp and amp/2, respectively.

Note

We’ll use the diff syntax below to mark which lines of code should be added, removed or left unchanged. Like this:

  this line should remain unchanged
- this line should be removed
+ this line should be added

We change the template to only contain the normalized pulse, with amplitude between -1 and 1:

-    data = amp * sin2(ns) * np.sin(2 * np.pi * freq * t)
+    data = sin2(ns) * np.sin(2 * np.pi * freq * t)

     pulse_1 = pls.setup_template(OUTPUT_PORT, group=0, template=data)

And instead we encode the amplitude into the scale LUT. Since we want to output two pulses with two different scales, we pass a list of two scales instead of one number:

-    pls.setup_scale_lut(OUTPUT_PORT, group=0, scales=1.0)
+    pls.setup_scale_lut(OUTPUT_PORT, group=0, scales=[amp, amp/2])

We are ready to modify the pulse sequence:

     T = 0.0
     pls.store(T)
     pls.output_pulse(T, pulse_1)
+    T += pulse_1.get_duration()
+    pls.next_scale(T, output_ports=OUTPUT_PORT, group=0)
+    pls.output_pulse(T, pulse_1)

We output the first pulse and start the sampling window at time T = 0, just like before. We now increase the time variable T by the duration of pulse_1. When the first pulse is over, we change the amplitude of the scaling block for OUTPUT_PORT and group 0 with the method next_scale(). We finally output the same pulse_1 again but, because we changed the scaling factor, the second pulse will have amplitude amp/2. At the beginning of the experiment (T = 0 for the first repetition), the scale has the value of the first entry in the LUT, i.e. amp in our example.

Here’s the result of a new run:

../../_images/pulsed_demo_2a_light.svg ../../_images/pulsed_demo_2a_dark.svg

Using frequency lookup tables

Similarly to scale LUTs, there are frequency LUTs to encode the frequency and phase of pulses allowing us to program sequences in a more compact way.

When we define a template, we indicate if we want to use the frequency LUT with the optional argument envelope. In the first example, we used the default value envelope=False because we encoded the frequency directly into the data points. Now, we instead encode only the envelope in the template:

-    data = sin2(ns) * np.sin(2 * np.pi * freq * t)
+    data = sin2(ns)

-    pulse_1 = pls.setup_template(OUTPUT_PORT, group=0, template=data)
+    pulse_1 = pls.setup_template(OUTPUT_PORT, group=0, template=data, envelope=True)

And indicate with envelope=True that we want to use the frequency LUT to encode the frequency and the phase.

     pls.setup_scale_lut(OUTPUT_PORT, group=0, scales=[amp, amp/2])
+    pls.setup_freq_lut(OUTPUT_PORT, group=0, frequencies=freq, phases=-np.pi/2)

The intermediate frequency (IF) generator attached to the frequency LUT produces a cosine wave with the frequency and phase defined in the LUT. At time T=0, it starts and produces the wave np.cos(2 * np.pi * freq * t + phase) that gets multiplied with all the templates on OUTPUT_PORT and group=0 with envelope=True.

In this example we choose a phase of -π/2 to generate a sine instead of a cosine.


If we run with the same pulse sequence as before:

    T = 0.0
    pls.store(T)
    pls.output_pulse(T, pulse_1)
    T += pulse_1.get_duration()
    pls.next_scale(T, output_ports=OUTPUT_PORT, group=0)
    pls.output_pulse(T, pulse_1)

we get a similar result as before:

../../_images/pulsed_demo_3a_light.svg ../../_images/pulsed_demo_3a_dark.svg

The subtle difference is that now the IF generator starts at time T = 0 and is free running, while in the previous example we hard-coded the oscillation in the template. This results in a different phase of the second pulse in the two cases: in the first example the phase resets to -π/2 with a glitch at the start of the second pulse, while in this second example the phase continues to evolve coherently across the two pulses.

We can force the phase of the IF generator to reset, in order to achieve the same result as before. We add a call to reset_phase(), which effectively resets the time for the IF generator:

     T = 0.0
     pls.store(T)
     pls.output_pulse(T, pulse_1)
     T += pulse_1.get_duration()
     pls.next_scale(T, output_ports=OUTPUT_PORT, group=0)
+    pls.reset_phase(T, output_ports=OUTPUT_PORT, group=0)
     pls.output_pulse(T, pulse_1)
../../_images/pulsed_demo_3b_light.svg ../../_images/pulsed_demo_3b_dark.svg

It is now easy to change the frequency and the phase of the second pulse. We add another value for frequency and phase for the second pulse in the frequency LUT:

-    pls.setup_freq_lut(OUTPUT_PORT, group=0, frequencies=freq, phases=-np.pi/2)
+    pls.setup_freq_lut(OUTPUT_PORT, group=0, frequencies=[freq, freq/2], phases=[-np.pi/2, +np.pi/2])

In the sequence, we call next_frequency() instead of reset_phase() to indicate to the IF generator that we want to use the second entry in the LUT for both the frequency and the phase:

     T = 0.0
     pls.store(T)
     pls.output_pulse(T, pulse_1)
     T += pulse_1.get_duration()
     pls.next_scale(T, output_ports=OUTPUT_PORT, group=0)
-    pls.reset_phase(T, output_ports=OUTPUT_PORT, group=0)
+    pls.next_frequency(T, output_ports=OUTPUT_PORT, group=0)
     pls.output_pulse(T, pulse_1)

At the end of the first pulse, the IF generator instantly switches to output a cosine with frequency freq/2 and phase π/2. The internal time of the IF generator is reset when we call next_frequency(). In other words, reset_phase() is implicit in next_frequency().

And this is what we measure:

../../_images/pulsed_demo_3c_light.svg ../../_images/pulsed_demo_3c_dark.svg

Flat pulses with smooth rise and fall

When we want to use a pulse that is longer than 1 μs, or when we want to change the duration of the pulse during a pulse sequence, we can use setup_flat_pulse() instead of setup_template(). We setup this pulse on the same output port and the same group, meaning this pulse will be multiplied with the same scale and frequency generator (we will pass envelope=True) as our previous templates.

     pulse_1 = pls.setup_template(OUTPUT_PORT, group=0, template=data, envelope=True)
+
+    pulse_2 = pls.setup_flat_pulse(
+        OUTPUT_PORT,
+        group=0,
+        duration=6 * duration,
+        amplitude=0.7,
+        rise_time=100e-9,
+        fall_time=100e-9,
+        envelope=True,
+    )

Now we just add this pulse to the pulse sequence. We output it right after the first pulse. For now, we don’t change the frequency or the scale to be able to see the effect of the flat_pulse more clearly.

     T = 0.0
     pls.store(T)
     pls.output_pulse(T, pulse_1)
     T += pulse_1.get_duration()
-    pls.next_scale(T, output_ports=OUTPUT_PORT, group=0)
-    pls.next_frequency(T, output_ports=OUTPUT_PORT, group=0)
-    pls.output_pulse(T, pulse_1)
+    pls.output_pulse(T, pulse_2)

This is what it looks like:

../../_images/pulsed_demo_4a_light.svg ../../_images/pulsed_demo_4a_dark.svg

It’s easy to change the duration of the flat part of the pulse during the pulse sequence. This is useful e.g. when sweeping the length of the pulse. Let’s add a shorter flat pulse right after the long one:

     T = 0.0
     pls.store(T)
     pls.output_pulse(T, pulse_1)
     T += pulse_1.get_duration()
     pls.output_pulse(T, pulse_2)
+    T += pulse_2.get_duration()
+    pulse_2.set_flat_duration(100e-9)
+    pls.output_pulse(T, pulse_2)
../../_images/pulsed_demo_4b_light.svg ../../_images/pulsed_demo_4b_dark.svg

Output pulses at different times

We are free to choose when we output pulses. We can delay the second pulse simply by changing the time when we output it. Let’s delay the second pulse by 100 ns by changing only the definition of the pulse sequence:

     T = 0.0
     pls.store(T)
     pls.output_pulse(T, pulse_1)
-    T += pulse_1.get_duration()
+    T += pulse_1.get_duration() + 100e-9
     pls.output_pulse(T, pulse_2)
-    T += pulse_2.get_duration()
-    pulse_2.set_flat_duration(100e-9)
-    pls.output_pulse(T, pulse_2)
../../_images/pulsed_demo_5a_light.svg ../../_images/pulsed_demo_5a_dark.svg

It is also possible to output many pulses at the same time. In this case make sure that the combined amplitude of the pulses doesn’t exceed ±1. As an example, we output pulse_1 again 200 ns after pulse_2 starts, somewhere in the middle of it:

     T = 0.0
     pls.store(T)
     pls.output_pulse(T, pulse_1)
     T += pulse_1.get_duration() + 100e-9
     pls.output_pulse(T, pulse_2)
+    T += 200e-9
+    pls.output_pulse(T, pulse_1)
../../_images/pulsed_demo_5b_light.svg ../../_images/pulsed_demo_5b_dark.svg

Note how we increase the time variable T by an arbitrary amount of 200 ns, rather than by the duration of pulse_2. The envelope of the resulting pulse is the sum of the envelopes of pulse_1 and pulse_2, where they overlap in time.


It is also possible to change the scale and frequency while a pulse is playing. For example, let’s change first the scale and then the frequency during the flat pulse. We only need to change the timing of the pulse sequence:

     T = 0.0
     pls.store(T)
     pls.output_pulse(T, pulse_1)
     T += pulse_1.get_duration() + 100e-9
     pls.output_pulse(T, pulse_2)
     T += 200e-9
     pls.output_pulse(T, pulse_1)
+    T += 300e-9
+    pls.next_scale(T, output_ports=OUTPUT_PORT, group=0)
+    T += 300e-9
+    pls.next_frequency(T, output_ports=OUTPUT_PORT, group=0)
../../_images/pulsed_demo_5c_light.svg ../../_images/pulsed_demo_5c_dark.svg

In general, the timing of when we modify the scales, frequencies or output pulses is completely up to us and clearly encoded in the pulse sequence. The only requirement is that all these events happen on a time grid with a resolution of 2 ns, equal to get_clk_T().

Complete code

If you made it this far and applied all the code changes of the previous sections, congratulations! You probably have in front of you a script that looks like the one below here. Keep in mind that you can also have a look at the various example scripts in the presto-demo repository.

import matplotlib.pyplot as plt
import numpy as np

from presto import pulsed
from presto.hardware import AdcMode, DacMode
from presto.utils import sin2, plot_sequence

OUTPUT_PORT = 1  # 1 to 16, can be a list
INPUT_PORT = 1  # 1 to 16, can be a list

with pulsed.Pulsed(
    address="192.168.20.23",
    ext_ref_clk=False,
    adc_mode=AdcMode.Direct,
    dac_mode=DacMode.Direct,
) as pls:
    pls.hardware.set_dac_current(OUTPUT_PORT, 40_500)  # μA, 2250 to 40500
    pls.hardware.set_inv_sinc(OUTPUT_PORT, 0)
    pls.hardware.set_adc_attenuation(INPUT_PORT, 0.0)  # dB, 0.0 to 27.0

    pls.setup_store(INPUT_PORT, duration=2e-6)

    amp = 0.5  # FS
    duration = 200e-9  # s
    freq = 42.5e6  # Hz

    ns = int(round(duration * pls.get_fs("dac")))
    t = np.linspace(0, duration, ns, endpoint=False)
    data = sin2(ns)

    pulse_1 = pls.setup_template(OUTPUT_PORT, group=0, template=data, envelope=True)
    pulse_2 = pls.setup_flat_pulse(
        OUTPUT_PORT,
        group=0,
        duration=6 * duration,
        amplitude=0.7,
        rise_time=100e-9,
        fall_time=100e-9,
        envelope=True,
    )

    pls.setup_scale_lut(OUTPUT_PORT, group=0, scales=[amp, amp / 2])
    pls.setup_freq_lut(
        OUTPUT_PORT,
        group=0,
        frequencies=[freq, freq / 2],
        phases=[-np.pi / 2, +np.pi / 2],
    )

    T = 0.0
    pls.store(T)
    pls.output_pulse(T, pulse_1)
    T += pulse_1.get_duration() + 100e-9
    pls.output_pulse(T, pulse_2)
    T += 200e-9
    pls.output_pulse(T, pulse_1)
    T += 300e-9
    pls.next_scale(T, output_ports=OUTPUT_PORT, group=0)
    T += 300e-9
    pls.next_frequency(T, output_ports=OUTPUT_PORT, group=0)

    # plot_sequence(pls, period=4e-6, repeat_count=1, num_averages=1)

    pls.run(period=4e-6, repeat_count=1, num_averages=1)
    t_arr, data = pls.get_store_data()


fig, ax = plt.subplots(tight_layout=True, figsize=(6, 2.5))
ax.plot(1e9 * t_arr, data[0, 0, :])
ax.set_xlabel("Time [ns]")
ax.set_ylabel("Input signal [FS]")
plt.show()