06 - Generators and Readouts
Objective: Learn how to use waveform generators and optimize readout resonators. This notebook covers:
Generator configuration and waveform styles
Readout resonator tuning and optimization
IQ mixing and signal processing
Frequency sweeps for resonator characterization
1. Setup
[1]:
# Standard imports
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import logging
from qick import *
from qick.asm_v2 import AveragerProgramV2
# Setup logging
logging.basicConfig(level=logging.INFO, format='%(levelname)-8s [%(filename)s:%(lineno)d] %(message)s')
logging.getLogger("qick_processor").setLevel(logging.WARNING)
# Connect to the board (adjust the path to your firmware)
BITSTREAM_PATH = 'qick_216.bit'#'/path/to/your/firmware.bit' # <--- CHANGE THIS
soc = QickSoc(BITSTREAM_PATH)
soccfg = soc
# Define hardware channels
GEN_CH = 0 # Generator channel (DAC)
RO_CH = 0 # Readout channel (ADC)
2. Understanding Waveform Generators
The QICK board has multiple DAC channels that can generate arbitrary waveforms. Each generator can produce:
Constant pulses (
style="const"): Square pulses with constant amplitudeArbitrary/envelope pulses (
style="arb"): Shaped pulses using a predefined envelopeFlat-top pulses (
style="flat_top"): Gaussian rise/fall with constant middle section
Let’s explore each type.
3. Generator Configuration: Constant Pulse
The simplest waveform is a constant pulse. It has a flat amplitude over its duration.
[2]:
class ConstantPulseProgram(AveragerProgramV2):
def _initialize(self, cfg):
self.declare_gen(ch=cfg['gen_ch'], nqz=1)
self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'])
# Constant pulse - square shape
self.add_pulse(
ch=cfg['gen_ch'],
name="const_pulse",
style="const",
freq=cfg['freq'], ro_ch=cfg['ro_ch'],
length=cfg['pulse_len'],
phase=cfg['phase'],
gain=cfg['gain']
)
self.add_readoutconfig(ch=cfg['ro_ch'], name="my_ro",
freq=cfg['freq'], gen_ch=cfg['gen_ch'])
self.send_readoutconfig(ch=cfg['ro_ch'], name="my_ro", t=0)
def _body(self, cfg):
self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'])
self.pulse(ch=cfg['gen_ch'], name="const_pulse", t=0)
# Configure and run
config_const = {
'gen_ch': GEN_CH,
'ro_ch': RO_CH,
'freq': 100.0,
'pulse_len': 0.2,
'phase': 0,
'gain': 0.8,
'trig_time': 0.25,
'ro_len': 0.5
}
prog = ConstantPulseProgram(soccfg, reps=1, final_delay=1.0, cfg=config_const)
iq_data = prog.acquire_decimated(soc, rounds=10)
time_axis = prog.get_time_axis(ro_index=0)
plt.figure(figsize=(10, 4))
plt.plot(time_axis, iq_data[0][:,0], label='I')
plt.plot(time_axis, iq_data[0][:,1], label='Q')
plt.legend()
plt.xlabel('Time (µs)')
plt.ylabel('Amplitude (a.u.)')
plt.title('Constant Pulse - I/Q Components')
plt.grid(True)
plt.show()
Observation: The constant pulse produces a square-shaped response. The I and Q components oscillate at the carrier frequency (100 MHz in this case).
4. Shaped Pulses with Envelopes
Shaped pulses reduce spectral leakage and are essential for qubit control. The QICK supports:
Gaussian envelopes: Smooth rise and fall
Flat-top pulses: Gaussian edges with a constant top (ideal for readout resonators)
[3]:
class ShapedPulseProgram(AveragerProgramV2):
def _initialize(self, cfg):
self.declare_gen(ch=cfg['gen_ch'], nqz=1)
self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'])
# Define a Gaussian envelope
# The envelope length should be sufficient to capture the full pulse
ramp_len = 0.15
self.add_gauss(
ch=cfg['gen_ch'],
name="gauss_env",
sigma=ramp_len / 8, # Steepness of the Gaussian
length=ramp_len,
even_length=True
)
# Gaussian pulse (arbitrary envelope)
self.add_pulse(
ch=cfg['gen_ch'], name="gauss_pulse",
style="arb", envelope="gauss_env",
freq=cfg['freq'], ro_ch=cfg['ro_ch'], phase=cfg['phase'], gain=cfg['gain']
)
# Flat-top pulse (good for readout)
self.add_pulse(
ch=cfg['gen_ch'], name="flattop_pulse",
style="flat_top", envelope="gauss_env",
freq=cfg['freq'], ro_ch=cfg['ro_ch'], length=0.2, phase=cfg['phase'], gain=cfg['gain']
)
self.add_readoutconfig(ch=cfg['ro_ch'], name="my_ro",
freq=cfg['freq'], gen_ch=cfg['gen_ch'])
self.send_readoutconfig(ch=cfg['ro_ch'], name="my_ro", t=0)
def _body(self, cfg):
self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'], ddr4=True)
self.pulse(ch=cfg['gen_ch'], name="gauss_pulse", t=0, tag='gauss')
self.pulse(ch=cfg['gen_ch'], name="flattop_pulse", t=0.5, tag='flattop')
config_shaped = {
'gen_ch': GEN_CH,
'ro_ch': RO_CH,
'freq': 100.0,
'phase': 0,
'gain': 0.8,
'trig_time': 0.1,
'ro_len': 1.0
}
prog = ShapedPulseProgram(soccfg, reps=1, final_delay=0.5, cfg=config_shaped)
iq_data = prog.acquire_decimated(soc, rounds=10)
time_axis = prog.get_time_axis(ro_index=0)
# Separate the two pulses
magnitude = np.abs(iq_data[0].dot([1, 1j]))
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
plt.plot(time_axis, magnitude, label='Magnitude')
plt.axvline(x=0.25, color='r', linestyle='--', alpha=0.5, label='Gaussian peak')
plt.axvline(x=0.75, color='g', linestyle='--', alpha=0.5, label='Flat-top peak')
plt.xlabel('Time (µs)')
plt.ylabel('Magnitude (a.u.)')
plt.title('Shaped Pulses: Magnitude')
plt.legend()
plt.grid(True)
plt.subplot(1, 2, 2)
plt.plot(time_axis, iq_data[0][:,0], label='I')
plt.plot(time_axis, iq_data[0][:,1], label='Q')
plt.xlabel('Time (µs)')
plt.ylabel('Amplitude (a.u.)')
plt.title('Shaped Pulses: I/Q Components')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
Key points about shaped pulses:
Gaussian pulses minimize spectral sidebands, reducing crosstalk
Flat-top pulses maintain constant amplitude at the top, ideal for readout resonators
The envelope is applied to the pulse shape, not the carrier frequency
5. IQ Mixing and Quadrature Signals
The QICK uses IQ mixing to generate arbitrary phase shifts. I (in-phase) and Q (quadrature) components are 90° apart. By adjusting the phase parameter, you can rotate the pulse in the IQ plane.
[4]:
class IQMixingProgram(AveragerProgramV2):
def _initialize(self, cfg):
self.declare_gen(ch=cfg['gen_ch'], nqz=1)
self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'])
# Same pulse at different phases
for phase in cfg['phases']:
self.add_pulse(
ch=cfg['gen_ch'],
name=f"pulse_{phase}",
style="const",
freq=cfg['freq'], ro_ch=cfg['ro_ch'],
length=cfg['pulse_len'],
phase=phase,
gain=cfg['gain']
)
self.add_readoutconfig(ch=cfg['ro_ch'], name="my_ro",
freq=cfg['freq'], gen_ch=cfg['gen_ch'])
self.send_readoutconfig(ch=cfg['ro_ch'], name="my_ro", t=0)
def _body(self, cfg):
self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'], ddr4=True)
for i, phase in enumerate(cfg['phases']):
self.pulse(ch=cfg['gen_ch'], name=f"pulse_{phase}", t=i * 0.3)
config_iq = {
'gen_ch': GEN_CH,
'ro_ch': RO_CH,
'freq': 500.0,
'pulse_len': 0.1,
'gain': 0.8,
'trig_time': 0.05,
'ro_len': 1.5,
'phases': [0, 45, 90, 135, 180, 225, 270, 315]
}
prog = IQMixingProgram(soccfg, reps=1, final_delay=0.5, cfg=config_iq)
iq_data = prog.acquire_decimated(soc, rounds=10)
time_axis = prog.get_time_axis(ro_index=0)
# Extract magnitude at each pulse center
pulse_centers = [0.15 + i * 0.3 for i in range(len(config_iq['phases']))]
idx_centers = [np.argmin(np.abs(time_axis - t)) for t in pulse_centers]
iq_at_centers = iq_data[0][idx_centers, :]
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
plt.plot(time_axis, iq_data[0][:,0], label='I')
plt.plot(time_axis, iq_data[0][:,1], label='Q')
plt.xlabel('Time (µs)')
plt.ylabel('Amplitude (a.u.)')
plt.title('IQ Mixing - Multiple Phases')
plt.legend()
plt.grid(True)
plt.subplot(1, 2, 2)
plt.plot(iq_at_centers[:,0], iq_at_centers[:,1], 'o-', markersize=8)
for i, phase in enumerate(config_iq['phases']):
plt.annotate(f'{phase}°', (iq_at_centers[i,0], iq_at_centers[i,1]))
plt.axhline(0, color='k', linestyle='-', alpha=0.3)
plt.axvline(0, color='k', linestyle='-', alpha=0.3)
plt.xlabel('I')
plt.ylabel('Q')
plt.title('IQ Constellation')
plt.axis('equal')
plt.grid(True)
plt.show()
Explanation of IQ mixing:
Phase shifts rotate the pulse vector in the IQ plane
A pulse with phase 0° appears on the I axis
A pulse with phase 90° appears on the Q axis
The constellation plot shows the relationship between I and Q for different phases
6. Readout Resonator Characterization
Readout resonators are used to measure qubit states. To optimize the readout, we need to find the resonator’s resonant frequency. This is done by sweeping the readout frequency and measuring the response.
[5]:
class ReadoutSweepProgram(AveragerProgramV2):
def _initialize(self, cfg):
self.declare_gen(ch=cfg['gen_ch'], nqz=1)
self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'])
# Define the readout pulse (flat-top recommended for readout)
ramp_len = 0.05
self.add_gauss(ch=cfg['gen_ch'], name="ramp",
sigma=ramp_len/8, length=ramp_len, even_length=True)
self.add_pulse(
ch=cfg['gen_ch'], name="readout_pulse",
style="flat_top", envelope="ramp",
freq=cfg['freq'], ro_ch=cfg['ro_ch'], length=cfg['pulse_len'], phase=0, gain=cfg['gain']
)
# Configure readout - freq will be swept
self.add_readoutconfig(ch=cfg['ro_ch'], name="my_ro",
freq=cfg['freq'], gen_ch=cfg['gen_ch'])
self.send_readoutconfig(ch=cfg['ro_ch'], name="my_ro", t=0)
def _body(self, cfg):
self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'], ddr4=True)
self.pulse(ch=cfg['gen_ch'], name="readout_pulse", t=0)
# Frequency sweep parameters
freqs = np.linspace(490, 510, 41) # MHz
results_i = []
results_q = []
base_config = {
'gen_ch': GEN_CH,
'ro_ch': RO_CH,
'pulse_len': 0.5,
'gain': 0.5,
'trig_time': 0.1,
'ro_len': 0.8
}
print("Sweeping readout frequency...")
for f in freqs:
config = base_config.copy()
config['freq'] = f
prog = ReadoutSweepProgram(soccfg, reps=1, final_delay=0.5, cfg=config)
iq_data = prog.acquire_decimated(soc, rounds=100) # 100 averages for clean data
# Average the I/Q data over the pulse duration
# (take mean after the pulse has settled)
start_idx = int(0.2 / prog.get_time_axis(ro_index=0)[1]) # 0.2 us after trigger
i_avg = np.mean(iq_data[0][start_idx:, 0])
q_avg = np.mean(iq_data[0][start_idx:, 1])
results_i.append(i_avg)
results_q.append(q_avg)
results_mag = np.sqrt(np.array(results_i)**2 + np.array(results_q)**2)
results_phase = np.arctan2(results_q, results_i) * 180 / np.pi
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
plt.plot(freqs, results_mag, 'o-', linewidth=2, markersize=4)
plt.xlabel('Frequency (MHz)')
plt.ylabel('|IQ| (a.u.)')
plt.title('Readout Resonator - Magnitude Response')
plt.grid(True)
plt.subplot(1, 2, 2)
plt.plot(freqs, results_phase, 'o-', linewidth=2, markersize=4)
plt.xlabel('Frequency (MHz)')
plt.ylabel('Phase (degrees)')
plt.title('Readout Resonator - Phase Response')
plt.grid(True)
plt.tight_layout()
plt.show()
# Find the resonance frequency (minimum magnitude for a dispersive readout)
resonance_idx = np.argmin(results_mag)
resonance_freq = freqs[resonance_idx]
print(f"\nResonance frequency: {resonance_freq:.2f} MHz")
print(f"Maximum magnitude: {results_mag[resonance_idx]:.3f} a.u.")
Sweeping readout frequency...
Resonance frequency: 509.50 MHz
Maximum magnitude: 390.140 a.u.
Interpreting the readout sweep:
The magnitude response shows a dip at the resonance frequency
The phase response shows a sharp transition (dispersion) at resonance
For optimal readout, you typically operate slightly detuned from resonance
The exact optimal point depends on your measurement protocol (e.g., homodyne vs. heterodyne)
7. Optimizing Readout Parameters
Once you find the resonance frequency, you can optimize other parameters like readout length and gain.
[6]:
class ReadoutOptimizationProgram(AveragerProgramV2):
def _initialize(self, cfg):
self.declare_gen(ch=cfg['gen_ch'], nqz=1)
self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'])
ramp_len = 0.05
self.add_gauss(ch=cfg['gen_ch'], name="ramp",
sigma=ramp_len/8, length=ramp_len, even_length=True)
self.add_pulse(
ch=cfg['gen_ch'], name="readout_pulse",
style="flat_top", envelope="ramp",
freq=cfg['freq'], ro_ch=cfg['ro_ch'], length=cfg['pulse_len'], phase=0, gain=cfg['gain']
)
self.add_readoutconfig(ch=cfg['ro_ch'], name="my_ro",
freq=cfg['freq'], gen_ch=cfg['gen_ch'])
self.send_readoutconfig(ch=cfg['ro_ch'], name="my_ro", t=0)
def _body(self, cfg):
self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'], ddr4=True)
self.pulse(ch=cfg['gen_ch'], name="readout_pulse", t=0)
# Use the found resonance frequency
optimal_freq = resonance_freq if 'resonance_freq' in dir() else 500.0
# Sweep readout length
lengths = np.linspace(0.2, 2.0, 19)
snr_results = []
print("Optimizing readout length...")
for l in lengths:
config = {
'gen_ch': GEN_CH,
'ro_ch': RO_CH,
'freq': optimal_freq,
'pulse_len': l,
'gain': 0.5,
'trig_time': 0.1,
'ro_len': l + 0.1 # Readout slightly longer than pulse
}
prog = ReadoutOptimizationProgram(soccfg, reps=1, final_delay=0.5, cfg=config)
iq_data = prog.acquire_decimated(soc, rounds=100)
# Estimate SNR from signal magnitude
signal_mag = np.abs(iq_data[0].dot([1, 1j]))
signal_peak = np.max(signal_mag)
noise_std = np.std(signal_mag[:50]) # Noise before the pulse
snr = signal_peak / noise_std if noise_std > 0 else 0
snr_results.append(snr)
plt.figure(figsize=(10, 5))
plt.plot(lengths, snr_results, 'o-', linewidth=2, markersize=6)
plt.xlabel('Readout Pulse Length (µs)')
plt.ylabel('Signal-to-Noise Ratio (SNR)')
plt.title('Readout Optimization - SNR vs. Pulse Length')
plt.grid(True)
plt.show()
optimal_idx = np.argmax(snr_results)
optimal_length = lengths[optimal_idx]
print(f"\nOptimal readout length: {optimal_length:.2f} µs")
print(f"Maximum SNR: {snr_results[optimal_idx]:.1f}")
Optimizing readout length...
Optimal readout length: 1.50 µs
Maximum SNR: 7927.4
8. Summary
You have learned:
How to configure generators with different pulse styles (constant, Gaussian, flat-top)
How IQ mixing works and how phase shifts affect the IQ plane
How to characterize a readout resonator via frequency sweeps
How to optimize readout parameters like pulse length
Key takeaways:
Use shaped pulses (Gaussian or flat-top) for better spectral purity
IQ mixing allows arbitrary phase control of your pulses
Readout resonators should be characterized before experiments
The optimal readout frequency is often slightly detuned from resonance
Next steps: Proceed to `07_Advanced_Generators_And_Readouts <./07_Advanced_Generators_And_Readouts.ipynb>`__ to learn about the DDR4 and MR buffers for high-speed data capture.