See also

An IPython notebook version of this tutorial can be downloaded here:

mixer_correction.ipynb

Mixer correction

In this tutorial we will demonstrate the ability to compensate for output mixer non-idealities and observe the changes using an oscilloscope.

Mixer non-idealities can lead to unwanted spurs on the output (LO/RF/IF feedthrough and other spurious products) and they can be compensated by applying adjustments to the I/Q outputs: phase offset, gain ratio and DC offset. This solution applies to both baseband QCM/QRM products using external mixers as well as QCM-RF and QRM-RF products.

The tutorial is designed for all Qblox baseband products: Pulsar QRM, Pulsar QCM, Cluster QRM, Cluster QCM. We use the term ‘QxM’ encompassing both QCM and QRM modules. We will adjust all the parameters listed above and observe the changes to the I/Q outputs directly on an oscilloscope.

For QCM-RF and QRM-RF products, one can refer to the ‘mixer calibration’ section of the tutorial on RF-control.

To run this tutorial please make sure you have installed and enabled ipywidgets:

pip install ipywidgets
jupyter nbextension enable –-py widgetsnbextension

Setup

First, we are going to import the required packages.

[1]:
# Import ipython widgets
import json
import math
import os

import ipywidgets as widgets
import matplotlib.pyplot
import numpy

# Set up the environment.
import scipy.signal
from IPython.display import display
from ipywidgets import fixed, interact, interact_manual, interactive

from qblox_instruments import Cluster, PlugAndPlay, Pulsar

Scan For Devices

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).

[2]:
# Scan for available devices and display
with PlugAndPlay() as p:
    # get info of all devices
    device_list = p.list_devices()
    device_keys = list(device_list.keys())

# create widget for names and ip addresses
connect = widgets.Dropdown(
    options=[(device_list[key]["description"]["name"]) for key in device_list.keys()],
    description="Select Device",
)
print(
    "The following widget displays all the existing modules that are connected to your PC which includes the Pulsar modules as well as a Cluster. Select the device you want to run the notebook on."
)
display(connect)
The following widget displays all the existing modules that are connected to your PC which includes the Pulsar modules as well as a Cluster. Select the device you want to run the notebook on.

Pulsar QxM

Run these cells after selecting the your Pulsar module. Skip to the Cluster QxM section below if you have selected a Cluster module.

[3]:
Pulsar.close_all()

# Retrieve device name and IP address
device_name = connect.value
device_number = connect.options.index(device_name)
ip_address = device_list[device_keys[device_number]]["identity"]["ip"]

# Connect to device
qxm = Pulsar(f"{device_name}", ip_address)
qxm.reset()  # reset QxM
print(f"{device_name} connected at {ip_address}")
print(qxm.get_system_state())
pulsar-qrm connected at 192.168.0.4
Status: OKAY, Flags: NONE, Slot flags: NONE

Skip to the next section (Setup Sequencer) if you are not using a cluster.

Cluster QxM

First we connect to the Cluster using its IP address. Go to the Pulsar QxM section if you are using a Pulsar.

[ ]:
# close all previous connections to the cluster
Cluster.close_all()

# Retrieve device name and IP address
device_name = connect.value
device_number = connect.options.index(device_name)
ip_address = device_list[device_keys[device_number]]["identity"]["ip"]

# connect to the cluster and reset
cluster = Cluster(device_name, ip_address)
cluster.reset()
print(f"{device_name} connected at {ip_address}")

We then find all available cluster modules to connect to them individually.

[ ]:
# Find all QRM/QCM modules
available_slots = {}
for module in cluster.modules:
    # if module is currently present in stack
    if cluster._get_modules_present(module.slot_idx):
        # check if QxM is RF or baseband
        if module.is_rf_type:
            available_slots[f"module{module.slot_idx}"] = ["QCM-RF", "QRM-RF"][
                module.is_qrm_type
            ]
        else:
            available_slots[f"module{module.slot_idx}"] = ["QCM", "QRM"][
                module.is_qrm_type
            ]

# List of all QxM modules present
connect_qxm = widgets.Dropdown(options=[key for key in available_slots.keys()])

print(available_slots)
# display widget with cluster modules
print()
print("Select the QxM module from the available modules in your Cluster:")
display(connect_qxm)

Finally, we connect to the selected Cluster module.

[13]:
# Connect to the cluster QxM module
module = connect_qxm.value
qxm = getattr(cluster, module)
print(f"{available_slots[connect_qxm.value]} connected")
print(cluster.get_system_state())
QCM connected
Status: OKAY, Flags: NONE, Slot flags: NONE

Setup Sequencer

The easiest way to view the influence of the mixer correction is to mix the NCO sin and cos with I and Q values of 1 (fullscale). The instrument output would be simple sinusoids with a 90deg phase offset and identical amplitude.

We use sequencer 0 to set I and Q values of 1 (fullscle) using DC offset and we mix those with the NCO signals.

[4]:
# Program sequence we will not use.
sequence = {"waveforms": {}, "weights": {}, "acquisitions": {}, "program": "stop"}
with open("sequence.json", "w", encoding="utf-8") as file:
    json.dump(sequence, file, indent=4)
    file.close()
qxm.sequencer0.sequence(sequence)

# Program fullscale DC offset on I & Q, turn on NCO and enable modulation.
qxm.sequencer0.offset_awg_path0(1.0)
qxm.sequencer0.offset_awg_path1(1.0)
qxm.sequencer0.nco_freq(10e6)
qxm.sequencer0.mod_en_awg(True)

Control sliders

Create control sliders for the parameters described in the introduction. Each time the value of a parameter is updated, the sequencer is automatically stopped from the embedded firmware for safety reasons and has to be manually restarted.

The sliders cover the valid parameter range. If the code below is modified to input invalid values, the firmware will not program the values.

Please connect the I/Q outputs to an oscilloscope and set to trigger continuously on the I channel at 0V. Execute the code below, move the sliders and observe the result on the oscilloscope.

[5]:
def set_offset_I(offset_I):
    qxm.out0_offset(offset_I)
    qxm.arm_sequencer(0)
    qxm.start_sequencer(0)


def set_offset_Q(offset_Q):
    qxm.out1_offset(offset_Q)
    qxm.arm_sequencer(0)
    qxm.start_sequencer(0)


def set_gain_ratio(gain_ratio):
    qxm.sequencer0.mixer_corr_gain_ratio(gain_ratio)
    qxm.arm_sequencer(0)
    qxm.start_sequencer(0)


def set_phase_offset(phase_offset):
    qxm.sequencer0.mixer_corr_phase_offset_degree(phase_offset)
    qxm.arm_sequencer(0)
    qxm.start_sequencer(0)


interact(
    set_offset_I, offset_I=widgets.FloatSlider(min=-1.0, max=1.0, step=0.01, start=0.0)
)
interact(
    set_offset_Q, offset_Q=widgets.FloatSlider(min=-1.0, max=1.0, step=0.01, start=0.0)
)
interact(
    set_gain_ratio,
    gain_ratio=widgets.FloatSlider(min=0.5, max=2.0, step=0.1, start=1.0),
)
interact(
    set_phase_offset,
    phase_offset=widgets.FloatSlider(min=-45.0, max=45.0, step=1.0, start=0.0),
)
[5]:
<function __main__.set_phase_offset(phase_offset)>

Stop

Finally, let’s stop the sequencers and close the instrument connection. One can also display a detailed snapshot containing the instrument parameters before closing the connection by uncommenting the corresponding lines.

[ ]:
# Stop sequencers.
qxm.stop_sequencer()

# Print sequencer status (should now say it is stopped).
print(qxm.get_sequencer_state(0))

# Uncomment the following to print an overview of the instrument parameters.
# Print an overview of the instrument parameters.
# print("Snapshot:")
# qxm.print_readable_snapshot(update=True)

# Close the instrument connection.
Pulsar.close_all()
Cluster.close_all()