See also

A Jupyter notebook version of this tutorial can be downloaded here.

TTL Pulse Generation#

This tutorial will demonstrate how to use the pulse generation capabilities of the Timetagging Module (QTM) in a Q1ASM sequencer program. Before going through this tutorial it is highly recommended to peruse the QTM Module page and the Timetag Sequencer page. In this tutorial we will learn how to:

  • Play simple TTL pulses on a 1 nanosecond timegrid

  • Place TTL pulses with 39 picosecond precision using the fine delay argument

  • Play the shortest possible TTL pulse on the QTM, 1 nanosecond wide

[1]:
from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from qblox_instruments.qcodes_drivers.io_channel_qtm import IOChannelQTM
    from qblox_instruments.qcodes_drivers.sequencer import Sequencer
from __future__ import annotations

from qcodes.instrument import find_or_create_instrument

from qblox_instruments import Cluster, ClusterType

Scan For Clusters#

We scan for the available devices connected via ethernet using the Plug & Play functionality of the Qblox Instruments package (see Plug & Play for more info).

!qblox-pnp list

[2]:
cluster_ip = "10.10.200.42"
cluster_name = "cluster0"

Connect to Cluster#

We now make a connection with the Cluster.

[3]:
cluster: Cluster = find_or_create_instrument(
    Cluster,
    recreate=True,
    name=cluster_name,
    identifier=cluster_ip,
    dummy_cfg=(
        {
            2: ClusterType.CLUSTER_QCM,
            4: ClusterType.CLUSTER_QRM,
            6: ClusterType.CLUSTER_QCM_RF,
            8: ClusterType.CLUSTER_QRM_RF,
            10: ClusterType.CLUSTER_QTM,
            12: ClusterType.CLUSTER_QRC,
            16: ClusterType.CLUSTER_QSM,
        }
        if cluster_ip is None
        else None
    ),
)

cluster.reset()
print(cluster.get_system_status())
Status: ERROR, Flags: FEEDBACK_NETWORK_CALIBRATION_FAILED, Slot flags: NONE

Get connected modules#

[4]:
# QTM modules
modules = cluster.get_connected_modules(lambda mod: mod.is_qtm_type)
# This uses the module of the correct type with the lowest slot index
module = list(modules.values())[0]

Helper function#

[5]:
def get_wait(wait_time: int, script: str = "") -> str:
    """
    Return long wait times as a series of wait statements, since wait has maximum argument 65535 ns.

    Parameters
    ----------
    wait_time : int
        The amount of wait time required. Minimum 4ns.
    script : string
        The Q1ASM script to which the waits must be added.

    Returns
    -------
        The series of decomposed wait instructions for the long wait.

    """
    for i in range(wait_time // 65532):
        script = script + "\n \twait    65532   #waiting time."
    if wait_time % 65532:
        script = script + f"\n \twait    {wait_time % 65532}    #waiting time."
    return script

Placing TTL pulses on 1ns timegrid.#

[6]:
# Defining the function to produce the correct sequence and module settings, upload the sequence, and arm the sequencer.


def digital_output(
    io: IOChannelQTM,
    seq: Sequencer,
    pulse_duration: int,
    wait_duration: int = 100,
    loop: bool = False,
    sync: bool = False,
) -> None:
    """
    Produce a pulse_duration ns long TTL pulse from the specified QTM channel.
    An infinite pulse train can also be produced for easy observation on a scope.

    Parameters
    ----------
    io : IOChannelQTM
        IOChannel being used.
    seq : Sequencer
        Corresponding sequencer being used.
    pulse_duration : int
        Duration of the TTL pulse being high in nanoseconds. Must be minimum 8 ns.
    wait_duration : int
        Duration in nanoseconds of the output being low after pulse ends. Must be minimum 8 ns.
    loop : bool
        Set to True if infinite pulse train is desired.
    sync : bool
        Set to True if SYNQ needs to be enabled; for example when using multiple channels. The function can be called multiple times before the sequencers are started.

    """
    io.mode("output")
    seq.sync_en(False)

    program = """"""

    if sync:
        seq.sync_en(True)
        program += """ wait_sync 4
            """

    if not loop:
        program += f"""

            set_digital 1,1,0  # arguments - level (1=high), mask (always 1), fine_delay in steps of 1/128 ns
            upd_param 4
            {get_wait(pulse_duration - 4)}
            set_digital 0,1,0 # arguments - level (0=low), mask (always 1), fine_delay in steps of 1/128 ns
            upd_param 4
            {get_wait(wait_duration - 4)}

            stop
            """

    elif loop:
        program += f"""

            loop:
                set_digital 1,1,0  #level (1=high), mask (always 1), fine_delay in steps of 1/128 ns
                upd_param 4
                {get_wait(pulse_duration - 4)}
                set_digital 0,1,0 #level (0=low), mask (always 1), fine_delay in steps of 1/128 ns
                upd_param 4
                {get_wait(wait_duration - 4)}
                jmp @loop

            stop
            """

    seq.sequence(
        {
            "waveforms": {},
            "weights": {},
            "acquisitions": {},
            "program": program,
        }
    )

    seq.arm_sequencer()
[7]:
# Using QTM Channel 2 only for output

digital_output(module.io_channel1, module.sequencer1, 100, loop=True)

module.start_sequencer()
[8]:
module.stop_sequencer()

Placing TTL pulses on 39 ps timegrid using fine delay#

Fine delay argument allows for shifting of the output’s rising or falling edge up to 16 ns in steps of 5/128 ns ~ 39 ps.

Here, we shorten the pulse by shifting only the rising edge.

Note that the fine_delay argument of set_digital is in units of 1/128 ~ 7.8 ps. Hence, the maximum pulse placement precision of 39 ps means one can effectively increase the fine_delay argument in steps of 5.

[9]:
def digital_output_shorten_pulse(
    io: IOChannelQTM,
    seq: Sequencer,
    fine_delay_step: float,
    sync: bool = False,
) -> None:
    """
    Produce 2 TTL pulses from the specified QTM channel initially separated by 50 ns.
    Each cycle the second rising edge moves forward by fine_delay_step ns.
    An infinite pulse train of these 2 pulses is produced for easy observation on a scope.

    Parameters
    ----------
    io : IOChannelQTM
        IOChannel being used.
    seq : Sequencer
        Corresponding sequencer being used.
    fine_delay_step : float
        Delay per cycle in the second rising edge in units of ns.
    sync : bool
        Set to True if SYNQ needs to be enabled; for example when using multiple channels. The function can be called multiple times before the sequencers are started.

    """
    io.mode("output")
    seq.sync_en(False)

    program = """"""

    if sync:
        seq.sync_en(True)
        program += """ wait_sync 4
        """

    # Converting the specified fine delay step into units of 1/128 ns
    fine_delay_step = int(fine_delay_step * 128)  # e.g. delay of 0.5 ns: fine_delay = 64

    program += f"""

        move 0, R1 #fine_delay
        move 1, R2
        move 0, R3

        loop:
            # First pulse, 50 ns long
            set_digital 1,1,0
            upd_param 10
            set_digital 0,1,0
            upd_param 4

            # Waiting for 50 ns
            wait 46

            # Here we set the fine_delay argument and increment it every cycle
            set_digital     R2,1,R1  #level (1=high), mask (always 1), fine_delay in steps of 1/128 ns
            upd_param       4
            wait            80
            set_digital     0,1,0
            upd_param       4
            {get_wait(60000000)}
            # Incrementing the fine delay asa specified
            add             R1, {fine_delay_step}, R1
            {get_wait(10000000)}
            jmp   @loop

        stop
        """

    seq.sequence(
        {
            "waveforms": {},
            "weights": {},
            "acquisitions": {},
            "program": program,
        }
    )

    seq.arm_sequencer()
[10]:
# Using QTM Channel 2 for output

digital_output_shorten_pulse(module.io_channel1, module.sequencer1, fine_delay_step=1 / 8)

module.start_sequencer()
[11]:
module.stop_sequencer()

In this part, following the example of the last section, we shift the whole pulse by delaying both the rising and falling edge of the digital signal by using the fine delay argument.

[12]:
def digital_output_shift_pulse(
    io: IOChannelQTM,
    seq: Sequencer,
    fine_delay_step: float,
    sync: bool = False,
) -> None:
    """
    Produce 2 TTL pulses from the specified QTM channel initially separated by 50 ns.
    Each cycle the entire second pulse moves forward by fine_delay_step ns.
    An infinite pulse train of these 2 pulses is produced for easy observation on a scope.

    Parameters
    ----------
    io : IOChannelQTM
        IOChannel being used.
    seq : Sequencer
        Corresponding sequencer being used.
    fine_delay_step : float
        Delay per cycle in the second pulse in units of ns
    sync : bool
        Set to True if SYNQ needs to be enabled; for example when using multiple channels.
            The function can be called multiple times before the sequencers are started.

    """
    io.mode("output")
    seq.sync_en(False)

    program = """"""

    if sync:
        seq.sync_en(True)
        program += """ wait_sync 4
            """

    fine_delay_step = int(fine_delay_step * 128)  # e.g. delay of 0.5 ns: fine_delay = 64

    program = f"""

        wait_sync 4
        move 0, R1 #fine_delay

        move 1, R2
        move 0, R3

        loop:
            # First pulse, 20 ns long
            set_digital     1,1,0  #level (1=high), mask (always 1), fine_delay in steps of 1/128 ns
            upd_param       20
            set_digital     0,1, 0
            upd_param       20

            # Wait 50 ns
            wait            30

            # Start of second pulse. Note that in contrast to the last example, now both rising and falling edges contain the fine delay argument
            set_digital     R2,1,R1  #level (1=high), mask (always 1), fine_delay in steps of 1/128 ns
            upd_param       20
            set_digital     R3,1,R1
            upd_param       20
            {get_wait(100000000)}

            # Increment the fine delay as specified
            add             R1, {fine_delay_step}, R1
            {get_wait(10000000)}
            jmp     @loop

        stop
        """

    seq.sequence(
        {
            "waveforms": {},
            "weights": {},
            "acquisitions": {},
            "program": program,
        }
    )

    seq.arm_sequencer()
[13]:
digital_output_shift_pulse(module.io_channel1, module.sequencer1, fine_delay_step=1 / 8)

module.start_sequencer()
[14]:
cluster.reset()

Minimum width (1 ns) pulse#

Here we produce a 1 ns pulse from the QTM via Q1ASM using fine delay.

[15]:
def digital_output_1ns(
    io: IOChannelQTM,
    seq: Sequencer,
    loop: bool = False,
    sync: bool = False,
) -> None:
    """
    Produce a 1 ns pulse from the specified QTM channel.
    An infinite pulse train of these pulses can be produced.

    Parameters
    ----------
    io : IOChannelQTM
        IOChannel being used.
    seq : Sequencer
        Corresponding sequencer being used.
    loop : bool
        Set to True if infinite pulse train is desired
    sync : bool
        Set to True if SYNQ needs to be enabled; for example when using multiple channels. The function can be called multiple times before the sequencers are started

    """
    io.mode("output")
    seq.sync_en(False)

    program = """"""

    if sync:
        seq.sync_en(True)
        program += """ wait_sync 4
            """

    if not loop:
        program += """

            # Introducing fine delay of 3ns in the rising edge
            set_digital 1,1,384  #level (1=high), mask (always 1), fine_delay in steps of 1/128 ns
            upd_param 4
            set_digital 0,1,0
            upd_param 396

            stop
            """

    elif loop:
        program += """

            loop:
                # Introducing fine delay of 3ns in the rising edge
                set_digital 1,1,384  #level (1=high), mask (always 1), fine_delay in steps of 1/128 ns
                upd_param 4
                set_digital 0,1,0
                upd_param 400
                jmp @loop

            stop
            """

    seq.sequence(
        {
            "waveforms": {},
            "weights": {},
            "acquisitions": {},
            "program": program,
        }
    )

    seq.arm_sequencer()
[16]:
digital_output_1ns(module.io_channel1, module.sequencer1, loop=True)

module.start_sequencer()

Stopping all sequencers and then resetting the cluster#

[17]:
module.stop_sequencer()
[18]:
cluster.reset()

print(cluster.get_system_status())
Status: ERROR, Flags: FEEDBACK_NETWORK_CALIBRATION_FAILED, Slot flags: NONE