Skip to content

Channels

In the QUAM library, channels are a fundamental concept that represent the physical connections to the quantum hardware. They are defined in the quam.components.channels module.

We distinguish between the following channel types, where the terms "output" and "input" are always from the perspective of the OPX hardware:

1. Analog output channels

2. Analog output + input channels

3. Digital channels

Each analog Channel corresponds to an element in QUA, whereas the digital channel is part of an analog channel.

These channel combinations cover most use cases, although there are exceptions (input-only channels and single-output, IQ-input channels) which will be implemented in a subsequent QUAM release. If you need such channels, please create a Github issue.

Analog Output Channels

Analog output channels are the primary means of controlling the quantum hardware. They can be used to send various types of signals, such as microwave or RF signals, to control the quantum system. The two types of analog output channels are the SingleChannel and the IQChannel.

Analog Channel Ports

A SingleChannel is always attached to a single OPX output port, and similarly an IQChannel has an associated pair of IQ ports:

from quam.components import SingleChannel, IQChannel

single_channel = SingleChannel(
    opx_output=("con1", 1),
    ...
)
IQ_channel = IQChannel(
    opx_output_I=("con1", 2),
    opx_output_Q=("con1", 3),
    ...
)

!!! warning "Port Properties in Channels (Deprecated)" Some properties such as opx_output_offset, opx_input_offset, filter_fir_taps, filter_iir_taps, shareable, and inverted are currently available as channel attributes for backwards compatibility. However, these properties belong to ports and should be configured through explicit Port objects.

**These properties are deprecated and will be removed in a future version.** Runtime deprecation warnings are now emitted when these properties are used, providing migration guidance. See the [Migration Guide](#migrating-from-channel-level-port-properties) below for details on how to update your code.

For more advanced port management, including port containers, port references, and hardware-specific configurations (LF-FEM, MW-FEM, OPX+), see the Channel Ports documentation.

For advanced control over port properties, define ports explicitly and reference them from channels:

from quam.components import BasicQuam, SingleChannel
from quam.components.ports import FEMPortsContainer

# Create QUAM instance with port container
machine = BasicQuam(ports=FEMPortsContainer())

# Define port with its properties
port = machine.ports.get_analog_output(
    "con1", 1, 2,  # controller, fem_id, port_id
    create=True,
    offset=0.15,  # DC offset configured on port
    shareable=True,  # Port sharing configured on port
    sampling_rate=2e9,
)

# Create channel that references the port
machine.channels["drive"] = SingleChannel(
    opx_output=port.get_reference()  # Reference to configured port
)

This approach ensures centralized port configuration and enables port sharing across channels. See Channel Ports for complete documentation.

DC Offset

Each analog channel can have a specified DC offset that remains for the duration of the QUA program.

Recommended Approach - Configure DC offset on the port:

from quam.components.ports import OPXPlusAnalogOutputPort

# For single channels
port = OPXPlusAnalogOutputPort("con1", 1, offset=0.15)
channel = SingleChannel(opx_output=port)

# For IQ channels with different I/Q offsets
port_I = OPXPlusAnalogOutputPort("con1", 2, offset=0.10)
port_Q = OPXPlusAnalogOutputPort("con1", 3, offset=0.12)
IQ_channel = IQChannel(opx_output_I=port_I, opx_output_Q=port_Q)

Deprecated Approach - Setting offset on channel (emits deprecation warning):

# Still works but will be removed in a future version
channel = SingleChannel(opx_output=("con1", 1), opx_output_offset=0.15)
IQ_channel = IQChannel(
    opx_output_I=("con1", 2),
    opx_output_Q=("con1", 3),
    opx_output_offset_I=0.10,
    opx_output_offset_Q=0.12
)

Note that if multiple channels are attached to the same OPX output port(s), they may not have different output offsets. This raises a warning and chooses the DC offset of the last channel.

The DC offset can also be modified while a QUA program is running:

from qm.qua import program

with program() as prog:
    single_channel.set_dc_offset(offset=0.1)
    IQ_channel.set_dc_offset(offset=0.25, element_input="I")  # Set offset of port I
The offsets can also be QUA variables. Channel.set_dc_offset() is a light wrapper around qm.qua.set_dc_offset to attach it to the channel.

Frequency Converters

The IQChannel is usually connected to a mixer to upconvert the signal using a local oscillator. This frequency upconversion is represented in QUAM by a FrequencyConverter

from quam.components.hardware import FrequencyConverter, LocalOscillator, Mixer

IQ_channel = IQChannel(
    opx_output_I=("con1", 2),
    opx_output_Q=("con1", 3),
    intermediate_frequency=100e6,  # Hz
    frequency_converter=FrequencyConverter(
        local_oscillator=LocalOscillator(frequency=6e9, power=10),
        mixer=Mixer(),
    )
)

Integrated frequency conversion systems such as QM's Octave usually have additional features such as auto-calibration. For this reason they have a specialized frequency converter such as the OctaveUpConverter. See the QUAM Octave Documentation documentation for details.

Analog Pulses

QUAM has a range of standard Pulse components in quam.components.pulses. These pulses can be registered as part of the analog channel via Channel.operations such that the channel can output the associated pulse waveforms:

from quam.components import pulses

channel.operations["X180"] = pulses.SquarePulse(
    amplitude=0.1,  # V
    length=16,  # ns
)

Once a pulse has been registered in a channel, it can be played within a QUA program:

with program() as prog:
    channel.play("X180")
Channel.play() is a light wrapper around qm.qua.play() to attach it to the channel.

Details on pulses in QUAM can be found at the Pulses Documentation.

Analog Output + Input Channels

Aside from sending signals to the quantum hardware, data is usually also received back, and subsequently read out through the hardware's input ports. In QUAM, this is represented using the InOutSingleChannel and the InOutIQChannel. These channels don't only have associated output port(s) but also input port(s):

from quam.components import InOutSingleChannel, InOutIQChannel

single_io_channel = InOutSingleChannel(
    opx_output=("con1", 1),
    opx_input=("con1", 1)
    ...
)
IQ_io_channel = InOutIQChannel(
    opx_output_I=("con1", 2),
    opx_output_Q=("con1", 3),
    opx_input_I=("con1", 1),
    opx_input_Q=("con1", 2)
    ...
)

These are extensions of the SingleChannel and the IQChannel that add relevant features for readout.

Both the InOutSingleChannel and the InOutIQChannel combine output + input as in most cases a signal is also sent to probe the quantum hardware. Support for input-only analog channels is planned for a future release.

Readout Pulses

Channels that have input ports can also have readout pulses:

from quam.components import pulses
io_channel.operations["readout"] = pulses.SquareReadoutPulse(
    length=16,  # ns
    amplitude=0.1,  # V
    integration_weights_angle=0.0,  # rad, optional rotation of readout signal
)
As can be seen, the readout pulse (in this case SquareReadoutPulse) is similar to the regular pulses, but with additional parameters for readout. Specifically, it contains the attributes integration_weights_angle and integration_weights to specify how the readout signal should be integrated.

Digital Channels

QUAM supports digital output channels (output from the OPX perspective) through the component DigitalOutputChannel. These can be added to any analog channel through the attribute Channel.digital_outputs. As an example:

from quam.components import SingleChannel, DigitalOutputChannel

analog_channel = SingleChannel(
    opx_output=("con1", 1),
    digital_outputs={
        "dig_out1": DigitalOutputChannel(opx_output=("con1", 1))
    }
)
The docstring of DigitalOutputChannel describes all the available properties.

Multiple digital outputs can be attached to the same analog channel:

analog_channel.digital_outputs = {
    "dig_out1": DigitalOutputChannel(opx_output=("con1", 1)),
    "dig_out2": DigitalOutputChannel(opx_output=("con1", 2)),
}
In this case, any digital pulses will be played to all digital channels.

Digital-only Channel

It is also possible to create a digital-only channel, i.e. using digital ports without any analog ports.

from quam.components import Channel, DigitalOutputChannel
channel = Channel(
    id="channel",
    digital_outputs={"1": DigitalOutputChannel(opx_output=("con1", 1))},
)

Digital Pulses

Once a DigitalOutputChannel is added to a Channel, digital waveforms can be played on it. This is done by attaching a digital waveform to a Pulse through the attribute Pulse.digital_marker:

from quam.components import pulses

pulse = pulses.SquarePulse(
    length=80,
    amplitude=0.2,
    digital_marker=[(1, 20), (0, 20), (1, 40)]
)
In the example above, the square pulse will also output digital waveform: "high" for 20 ns ⇨ "low" for 20 ns ⇨ "high" for 40 ns. This digital waveform will be played on all digital channels that are attached to the analog channel.

Digital-only Pulses

A digital pulse can also be played without a corresponding analog pulse. This can be done by directly using the base pulses.Pulse class:

channel.operations["digital"] = pulses.Pulse(length=100, digital_marker=[(1, 20, 0, 10)])

Sticky channels

A channel can be set to be sticky, meaning that the voltage after a pulse will remain at the last value of the pulse. Details can be found in the Sticky channel QUA documentation. Any channel can be made sticky by adding the channels.StickyChannelAddon to it:

from quam.components.channels import StickyChannelAddon

channel.sticky = StickyChannelAddon(duration=...)

Time Tagging

Time tagging is a feature that allows for the measurement of the time of arrival of a signal. It is implemented as the TimeTaggingAddon to the InSingleChannel.

To use the time tagging feature, the TimeTaggingAddon must be added to the InSingleChannel:

from quam.components.channels import InSingleChannel, TimeTaggingAddon

channel = InSingleChannel(
    id="channel",
    opx_input=("con1", 1),
    time_tagging=TimeTaggingAddon(
        signal_threshold=0.195,  # in units of V
        signal_polarity="below",
        derivative_threshold=0.073,  # in units of V/ns
        derivative_polarity="below",
    )
)
All parameters are optional, and are by default set to the values shown above.

Once the time tagging addon is added, the InSingleChannel.measure_time_tagging() method can be used within a QUA program to measure the time of arrival of the signal:

times, counts = channel.measure_time_tagging(size=1000, max_duration=3000)

  • The size parameter specifies the maximum number of samples to collect.
  • The max_duration parameter specifies the maximum duration to collect samples for.

Two QUA variables are returned:

  • times is a QUA array containing the times of arrival of the signal. It will contain at most size entries, though it may contain fewer if the maximum duration is reached first.
  • counts is a QUA integer containing the number of measured events, being at most equal to size.

Additional information on time tagging can be found in the Time Tagging QUA documentation.

Migrating from Channel-Level Port Properties

As of QUAM v0.5.0, port-related properties on channels are deprecated. This section provides guidance on migrating your code to use explicit Port objects instead.

Why Migrate?

Port properties such as opx_output_offset, filter_fir_taps, shareable, and inverted logically belong to ports, not channels. Using explicit Port objects:

  • Clarifies ownership: Properties are configured where they belong
  • Enables port sharing: Multiple channels can reference the same configured port
  • Centralizes configuration: Port containers provide unified port management
  • Prepares for future: Channel-level properties will be removed in a future version

Migration Examples

DC Offsets

Before (Deprecated):

channel = SingleChannel(
    opx_output=("con1", 1),
    opx_output_offset=0.15
)

After (Recommended):

from quam.components.ports import OPXPlusAnalogOutputPort

port = OPXPlusAnalogOutputPort("con1", 1, offset=0.15)
channel = SingleChannel(opx_output=port)

Filter Configuration

Before (Deprecated):

channel = SingleChannel(
    opx_output=("con1", 1),
    filter_fir_taps=[0.1, 0.2, 0.3],
    filter_iir_taps=[0.5, 0.6]
)

After (Recommended):

from quam.components.ports import OPXPlusAnalogOutputPort

port = OPXPlusAnalogOutputPort(
    "con1", 1,
    feedforward_filter=[0.1, 0.2, 0.3],
    feedback_filter=[0.5, 0.6]
)
channel = SingleChannel(opx_output=port)

Digital Channel Properties

Before (Deprecated):

digital_channel = DigitalOutputChannel(
    opx_output=("con1", 1),
    shareable=True,
    inverted=True
)

After (Recommended):

from quam.components.ports import OPXPlusDigitalOutputPort

port = OPXPlusDigitalOutputPort("con1", 1, shareable=True, inverted=True)
digital_channel = DigitalOutputChannel(opx_output=port)

IQ Channels with Offsets

Before (Deprecated):

IQ_channel = IQChannel(
    opx_output_I=("con1", 2),
    opx_output_Q=("con1", 3),
    opx_output_offset_I=0.10,
    opx_output_offset_Q=0.12
)

After (Recommended):

from quam.components.ports import OPXPlusAnalogOutputPort

port_I = OPXPlusAnalogOutputPort("con1", 2, offset=0.10)
port_Q = OPXPlusAnalogOutputPort("con1", 3, offset=0.12)
IQ_channel = IQChannel(opx_output_I=port_I, opx_output_Q=port_Q)

Using Port Containers (Best Practice)

For complex systems with many ports, use a port container for centralized management:

from quam.components import BasicQuam
from quam.components.ports import FEMPortsContainer

# Set up port container
machine = BasicQuam(ports=FEMPortsContainer())

# Create ports with properties
output_port = machine.ports.get_analog_output(
    "con1", 1, 2,  # controller, fem_id, port_id
    create=True,
    offset=0.15,
    shareable=True
)

# Reference port in channel
machine.channels["drive"] = SingleChannel(
    opx_output=output_port.get_reference()
)

See the Channel Ports documentation for more details on port containers and advanced port management.