07 - Advanced Generators and Readouts in tProc v2
Objective: Explore the advanced generator and readout blocks available in the QICK firmware. This notebook covers:
Muxed Generators (full-speed and mixer-mux) with Muxed Readouts (PFB)
Interpolated Generators vs. Standard Readouts (frequency matching in tProc v2 vs. v1)
Dynamically Configured Readouts for frequency sweeps and electrical delay calibration
Fast Dynamic Readout for phase-reset applications
IQ offset measurement and correction
Performance benchmarks
Real-time phase tracking
Common pitfalls and troubleshooting
Best practices summary
Prerequisites: Complete the introductory tutorials (`00_Getting_Started.ipynb <./00_Getting_Started.ipynb>`__ through `06_Generators_And_Readouts.ipynb <./06_Generators_And_Readouts.ipynb>`__).
1. Setup
[2]:
# Standard imports
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import logging
import time
from qick import *
from qick.asm_v2 import AveragerProgramV2, QickSweep1D, AsmV2
# Setup logging
logging.basicConfig(level=logging.INFO, format='%(levelname)-8s [%(filename)s:%(lineno)d] %(message)s')
# Connect to the board (adjust path as needed)
BITSTREAM_PATH = 'qick_216.bit'#'/path/to/your/firmware.bit' # <--- CHANGE THIS
soc = QickSoc(BITSTREAM_PATH)
soccfg = soc
# Define channel constants (adjust based on your firmware configuration)
FSGEN_CH = 0 # Full-speed generator (single tone, max bandwidth)
INTGEN_CH = [1, 2] # Interpolated generator (fine frequency resolution)
FSMUXGEN_CH = 3 # Full-speed mux generator (8 tones, phase coherent, up to 10 Gsps)
MIXMUXGEN_CH = 4 # Mixer-mux generator (8 tones, phase coherent, up to 7 Gsps, better SFDR)
DYNRO_CH = 0 # Dynamically configured readout (axis_dyn_readout_v1)
FASTDYNRO_CH = 1 # Fast dynamic readout (axis_readout_v3, for phase-reset applications)
MUXRO_CH = [2, 3, 4, 5] # Muxed readout channels (PFB, 4 outputs used here)
STDRO_CH = 6 # Standard readout (axis_readout_v2, PYNQ-configured)
print("Connected to QICK board")
print(f"Available generators (DACs): {list(range(len(soccfg['gens'])))}")
print(f"Available readouts (ADCs): {list(range(len(soccfg['readouts'])))}")
print(f"tProc core version: {soccfg['tprocs'][0]['version']}")
print(f"Sofware version: {soccfg['sw_version']}")
Connected to QICK board
Available generators (DACs): [0, 1, 2, 3, 4]
Available readouts (ADCs): [0, 1, 2, 3, 4, 5, 6]
tProc core version: 2.0
Sofware version: 0.2.381
[3]:
print("=== Readouts (ADCs) Hardware Identification ===")
for i, ro in enumerate(soccfg['readouts']):
ro_kind = ro.get('ro_type', 'Unknown RO')
buf_kind = ro.get('avgbuf_type', 'Unknown Buffer')
ro_ver = ro.get('ro_version', 'N/A')
print(f"RO {i}:")
print(f" - Readout Type: {ro_kind} (v{ro_ver})")
print(f" - Buffer Type: {buf_kind}")
print(f" - Decimation: {ro.get('decimation', 'N/A')}")
print(f" - Sampling Freq: {ro.get('fs', 'N/A')} MHz")
print("-" * 30)
=== Readouts (ADCs) Hardware Identification ===
RO 0:
- Readout Type: axis_dyn_readout_v1 (v1.0)
- Buffer Type: axis_avg_buffer
- Decimation: 1
- Sampling Freq: 2457.6 MHz
------------------------------
RO 1:
- Readout Type: axis_readout_v3 (v1.0)
- Buffer Type: axis_avg_buffer
- Decimation: 1
- Sampling Freq: 2457.6 MHz
------------------------------
RO 2:
- Readout Type: axis_pfb_readout_v3 (v1.0)
- Buffer Type: axis_avg_buffer
- Decimation: 2
- Sampling Freq: 2457.6 MHz
------------------------------
RO 3:
- Readout Type: axis_pfb_readout_v3 (v1.0)
- Buffer Type: axis_avg_buffer
- Decimation: 2
- Sampling Freq: 2457.6 MHz
------------------------------
RO 4:
- Readout Type: axis_pfb_readout_v3 (v1.0)
- Buffer Type: axis_avg_buffer
- Decimation: 2
- Sampling Freq: 2457.6 MHz
------------------------------
RO 5:
- Readout Type: axis_pfb_readout_v3 (v1.0)
- Buffer Type: axis_avg_buffer
- Decimation: 2
- Sampling Freq: 2457.6 MHz
------------------------------
RO 6:
- Readout Type: axis_readout_v2 (v1.0)
- Buffer Type: axis_avg_buffer
- Decimation: 1
- Sampling Freq: 2457.6 MHz
------------------------------
2. Generator and Readout Types Overview
Understanding the different generator and readout types is crucial for optimal performance.
[20]:
print("""
╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
║ GENERATOR TYPES ║
╠════════════════════════╦═════════════════════════════════════════════════════════════════════════════════════╣
║ Generator Type ║ Best For ║
╠════════════════════════╬═════════════════════════════════════════════════════════════════════════════════════╣
║ Full-Speed (FS) ║ Single tone, maximum bandwidth (10 Gsps), needs more PL logic ║
║ Interpolated (INT) ║ Fine frequency resolution, needs mixer_freq for upconversion, less PL logic ║
║ Full-Speed Mux (FSMUX) ║ Multi-tone, phase-coherent, up to 8 tones @ 10 Gsps, constant envelope ║
║ Mixer-Mux (MIXMUX) ║ Multi-tone, phase-coherent, lower sample rate (7 Gsps), better SFDR ║
╠════════════════════════╩═════════════════════════════════════════════════════════════════════════════════════╣
║ READOUT TYPES ║
╠════════════════════════╦═════════════════════════════════════════════════════════════════════════════════════╣
║ Readout Type ║ Best For ║
╠════════════════════════╬═════════════════════════════════════════════════════════════════════════════════════╣
║ Standard (STD) ║ Fixed frequency readout, simple averaging ║
║ Dynamic (DYN) ║ Sweeping frequencies, resonator spectroscopy, delay calibration ║
║ Fast Dynamic (FASTDYN) ║ Phase-reset applications, fast sequencing, qubit readout ║
║ Muxed PFB (MUX) ║ Multi-tone readout, 64 channels, simultaneous demodulation ║
╚════════════════════════╩═════════════════════════════════════════════════════════════════════════════════════╝
""")
╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
║ GENERATOR TYPES ║
╠════════════════════════╦═════════════════════════════════════════════════════════════════════════════════════╣
║ Generator Type ║ Best For ║
╠════════════════════════╬═════════════════════════════════════════════════════════════════════════════════════╣
║ Full-Speed (FS) ║ Single tone, maximum bandwidth (10 Gsps), simple pulses ║
║ Interpolated (INT) ║ Fine frequency resolution, needs mixer_freq for upconversion, shaped pulses ║
║ Full-Speed Mux (FSMUX) ║ Multi-tone, phase-coherent, up to 8 tones @ 10 Gsps, constant envelope ║
║ Mixer-Mux (MIXMUX) ║ Multi-tone, phase-coherent, lower sample rate (7 Gsps), better SFDR ║
╠════════════════════════╩═════════════════════════════════════════════════════════════════════════════════════╣
║ READOUT TYPES ║
╠════════════════════════╦═════════════════════════════════════════════════════════════════════════════════════╣
║ Readout Type ║ Best For ║
╠════════════════════════╬═════════════════════════════════════════════════════════════════════════════════════╣
║ Standard (STD) ║ Fixed frequency readout, simple averaging ║
║ Dynamic (DYN) ║ Sweeping frequencies, resonator spectroscopy, delay calibration ║
║ Fast Dynamic (FASTDYN) ║ Phase-reset applications, fast sequencing, qubit readout ║
║ Muxed PFB (MUX) ║ Multi-tone readout, 64 channels, simultaneous demodulation ║
╚════════════════════════╩═════════════════════════════════════════════════════════════════════════════════════╝
3. Muxed Generators and Muxed Readouts (PFB)
Muxed generators allow you to play multiple tones simultaneously, and muxed readouts (PFB) can demodulate them efficiently.
Key concepts:
The
maskparameter inadd_pulseselects which tones are playedThe PFB readout provides finer channel spacing than standard muxed readout (64 slices vs 8)
Linking generators to readouts with
gen_chenables automatic frequency matching
[47]:
class MuxProgram(AveragerProgramV2):
"""
Program demonstrating muxed generator with PFB readout.
Plays multiple tones simultaneously on a single DAC channel.
"""
def _initialize(self, cfg):
ro_chs = cfg['ro_chs']
gen_ch = cfg['gen_ch']
# Declare a muxed generator with multiple tones
# The 'ro_ch' argument links it to a readout for frequency matching
self.declare_gen(ch=gen_ch, nqz=1, ro_ch=ro_chs[0],
mixer_freq=cfg['mixer_freq'],
mux_freqs=cfg['pulse_freqs'],
mux_gains=cfg['pulse_gains'],
mux_phases=cfg['pulse_phases'])
# Declare each PFB readout channel, linking back to the same generator
# This automatically matches readout frequencies to generator tones
for ch, f, ph in zip(cfg['ro_chs'], cfg['pulse_freqs'], cfg['ro_phases']):
self.declare_readout(ch=ch, length=cfg['ro_len'], freq=f, phase=ph, gen_ch=gen_ch)
# Create a constant pulse that plays on all 4 tones simultaneously
# The 'mask' parameter selects which tone indices to enable
self.add_pulse(ch=gen_ch, name="mymux",
style="const",
length=cfg["pulse_len"],
mask=[0, 1, 2, 3],
)
def _body(self, cfg):
# Trigger readouts to start taking data before the pulse
self.trigger(ros=cfg['ro_chs'], pins=[0], t=cfg['trig_time'], ddr4=True)
# Play the pulse
self.pulse(ch=cfg['gen_ch'], name="mymux", t=0)
# Configuration for the program
config_mux = {
'gen_ch': MIXMUXGEN_CH, # we can use FSMUXGEN_CH here if we want higher sample rate but worse SFDR
'ro_chs': MUXRO_CH,
'mixer_freq': 500,
'pulse_freqs': [500, 550, 600, 650], # MHz
'pulse_gains': [1.0] * 4,
'pulse_phases': [0.0] * 4,
'ro_phases': [0.0] * 4, # Initial downconversion phases
'trig_time': 0.4, # µs, trigger before pulse
'pulse_len': 1.0, # µs
'ro_len': 1.9, # µs, readout length
}
print("Running muxed generator with PFB readout...")
prog = MuxProgram(soccfg, reps=1, final_delay=0.5, cfg=config_mux)
iq_list = prog.acquire_decimated(soc, rounds=1)
t = prog.get_time_axis(ro_index=0)
# Plot results for each readout channel
fig, axes = plt.subplots(len(config_mux['ro_chs']), 1, figsize=(12, 12))
for i, ch in enumerate(config_mux['ro_chs']):
axes[i].plot(t, iq_list[i][:, 0], label="I")
axes[i].plot(t, iq_list[i][:, 1], label="Q")
axes[i].plot(t, np.abs(iq_list[i].dot([1, 1j])), label="Magnitude")
axes[i].legend()
axes[i].set_ylabel(f"Tone {i} ({config_mux['pulse_freqs'][i]} MHz)")
axes[i].set_xlabel("Time (µs)")
plt.suptitle("Muxed Generator with PFB Readout")
plt.tight_layout()
INFO [tproc.py:515] PROCESSOR_STOP
INFO [tproc.py:515] PROCESSOR_STOP
Running muxed generator with PFB readout...
INFO [tproc.py:662] tProc pmem: loading data
INFO [tproc.py:695] tProc pmem: readback OK
INFO [tproc.py:662] tProc wmem: loading data
INFO [tproc.py:695] tProc wmem: readback OK
INFO [tproc.py:524] PROCESSOR_RESET
INFO [tproc.py:515] PROCESSOR_STOP
INFO [tproc.py:512] PROCESSOR_START
INFO [tproc.py:515] PROCESSOR_STOP
3.1 Correcting IQ Phase Offsets
The raw IQ data is rotated due to cable delays and mixer imperfections. We can correct this by measuring the average phase and applying a correction to either the upconversion (pulse) phase or the downconversion (readout) phase.
[30]:
# Calculate average phase offset for each channel (in degrees)
phases = [np.angle(iq.mean(axis=0).dot([1, 1j]), deg=True) for iq in iq_list]
print(f"Phase offsets: {phases}")
# Option 1: Rotate the upconversion phase
config_mux['pulse_phases'] = [-x for x in phases]
prog = MuxProgram(soccfg, reps=1, final_delay=0.5, cfg=config_mux)
iq_list_corrected_up = prog.acquire_decimated(soc, rounds=1)
# Option 2: Rotate the downconversion phase
config_mux['pulse_phases'] = [0.0] * 4
config_mux['ro_phases'] = phases
prog = MuxProgram(soccfg, reps=1, final_delay=0.5, cfg=config_mux)
iq_list_corrected_down = prog.acquire_decimated(soc, rounds=1)
# Plot corrected data (Option 2 as example)
fig, axes = plt.subplots(len(config_mux['ro_chs']), 1, figsize=(12, 10))
for i, ch in enumerate(config_mux['ro_chs']):
axes[i].plot(t, iq_list_corrected_down[i][:, 0], label="I")
axes[i].plot(t, iq_list_corrected_down[i][:, 1], label="Q")
axes[i].plot(t, np.abs(iq_list_corrected_down[i].dot([1, 1j])), label="Magnitude")
axes[i].legend()
axes[i].set_ylabel(f"Tone {i} (Phase-corrected)")
axes[i].set_xlabel("Time (µs)")
plt.suptitle("Phase-Corrected Muxed Readout")
plt.tight_layout()
INFO [tproc.py:515] PROCESSOR_STOP
INFO [tproc.py:515] PROCESSOR_STOP
Phase offsets: [-83.12613786622823, -130.43998631084776, 60.805937906847866, 15.380139289356636]
INFO [tproc.py:662] tProc pmem: loading data
INFO [tproc.py:695] tProc pmem: readback OK
INFO [tproc.py:662] tProc wmem: loading data
INFO [tproc.py:695] tProc wmem: readback OK
INFO [tproc.py:524] PROCESSOR_RESET
INFO [tproc.py:515] PROCESSOR_STOP
INFO [tproc.py:512] PROCESSOR_START
INFO [tproc.py:515] PROCESSOR_STOP
INFO [tproc.py:515] PROCESSOR_STOP
INFO [tproc.py:515] PROCESSOR_STOP
INFO [tproc.py:662] tProc pmem: loading data
INFO [tproc.py:695] tProc pmem: readback OK
INFO [tproc.py:662] tProc wmem: loading data
INFO [tproc.py:695] tProc wmem: readback OK
INFO [tproc.py:524] PROCESSOR_RESET
INFO [tproc.py:515] PROCESSOR_STOP
INFO [tproc.py:512] PROCESSOR_START
INFO [tproc.py:515] PROCESSOR_STOP
3.2 Mixer-Mux Generator Example
The mixer-mux generator uses a digital mixer-based architecture that provides better SFDR at the cost of lower sample rate.
[34]:
# Using the same MuxProgram class with mixer-mux generator
config_mixmux = {
'gen_ch': MIXMUXGEN_CH,
'ro_chs': MUXRO_CH,
'mixer_freq': 500,
'pulse_freqs': [60, 100, 150, 400], # MHz (lower frequencies work better)
'pulse_gains': [0.8] * 4, # Slightly lower gain for better linearity
'pulse_phases': [0.0] * 4,
'ro_phases': [0.0] * 4,
'trig_time': 0.4,
'pulse_len': 1.0,
'ro_len': 1.9,
}
print("Running mixer-mux generator...")
prog_mixmux = MuxProgram(soccfg, reps=1, final_delay=0.5, cfg=config_mixmux)
iq_list_mixmux = prog_mixmux.acquire_decimated(soc, rounds=1)
# Plot results
fig, axes = plt.subplots(len(config_mixmux['ro_chs']), 1, figsize=(12, 10))
for i in range(len(config_mixmux['ro_chs'])):
axes[i].plot(t, np.abs(iq_list_mixmux[i].dot([1, 1j])), label=f"Channel {i}")
axes[i].set_ylabel(f"Magnitude (Tone {i})")
axes[i].set_xlabel("Time (µs)")
axes[i].legend()
plt.suptitle("Mixer-Mux Generator Performance")
plt.tight_layout()
INFO [tproc.py:515] PROCESSOR_STOP
INFO [tproc.py:515] PROCESSOR_STOP
Running mixer-mux generator...
INFO [tproc.py:662] tProc pmem: loading data
INFO [tproc.py:695] tProc pmem: readback OK
INFO [tproc.py:662] tProc wmem: loading data
INFO [tproc.py:695] tProc wmem: readback OK
INFO [tproc.py:524] PROCESSOR_RESET
INFO [tproc.py:515] PROCESSOR_STOP
INFO [tproc.py:512] PROCESSOR_START
INFO [tproc.py:515] PROCESSOR_STOP
3.3 Playing Different Tone Combinations
You can selectively enable tones using different masks.
[52]:
class MuxProgramSelective(AveragerProgramV2):
"""Program demonstrating selective tone playback using masks."""
def _initialize(self, cfg):
ro_chs = cfg['ro_chs']
gen_ch = cfg['gen_ch']
self.declare_gen(ch=gen_ch, nqz=1, ro_ch=ro_chs[0],
mixer_freq=cfg['mixer_freq'],
mux_freqs=cfg['pulse_freqs'],
mux_gains=cfg['pulse_gains'],
mux_phases=cfg['pulse_phases'])
for ch, f, ph in zip(cfg['ro_chs'], cfg['pulse_freqs'], cfg['ro_phases']):
self.declare_readout(ch=ch, length=cfg['ro_len'], freq=f, phase=ph, gen_ch=gen_ch)
# Add multiple pulses with different masks
self.add_pulse(ch=gen_ch, name="tones_even",
style="const", length=cfg["pulse_len"],
mask=[0, 2]) # Even-indexed tones
self.add_pulse(ch=gen_ch, name="tones_odd",
style="const", length=cfg["pulse_len"],
mask=[1, 3]) # Odd-indexed tones
def _body(self, cfg):
self.trigger(ros=cfg['ro_chs'], pins=[0], t=cfg['trig_time'], ddr4=True)
# First pulse
self.pulse(ch=cfg['gen_ch'], name="tones_even", t=0)
# Second pulse starts after first finishes
self.pulse(ch=cfg['gen_ch'], name="tones_odd", t=cfg['pulse_len'] + 0.1)
config_selective = {
'gen_ch': MIXMUXGEN_CH, # we can use FSMUXGEN_CH here if we want higher sample rate but worse SFDR
'ro_chs': MUXRO_CH[:4],
'mixer_freq': 500,
'pulse_freqs': [50, 150, 200, 350],
'pulse_gains': [1.0] * 4,
'pulse_phases': [0.0] * 4,
'ro_phases': [0.0] * 4,
'trig_time': 0.4,
'pulse_len': 1.0,
'ro_len': 2.5,
}
print("Running selective tone playback...")
prog_selective = MuxProgramSelective(soccfg, reps=1, final_delay=0.5, cfg=config_selective)
iq_list_selective = prog_selective.acquire_decimated(soc, rounds=1)
npts = iq_list_selective[0].shape[0]
t = np.linspace(0, config_selective['ro_len'], npts)
# Plot showing two pulse sequences
npts = iq_list_selective[0].shape[0]
t = np.linspace(0, config_selective['ro_len'], npts)
fig, axes = plt.subplots(2, 1, figsize=(12, 10))
for i, ch in enumerate(config_selective['ro_chs']):
mag = np.abs(iq_list_selective[i].dot([1, 1j]))
axes[0].plot(t, mag, label=f"Tone {i} ({config_selective['pulse_freqs'][i]} MHz)")
axes[0].set_ylabel("Magnitude")
axes[0].set_xlabel("Time (µs)")
axes[0].legend()
axes[0].set_title("Selective Tone Playback: Even tones first, then odd tones")
axes[0].grid(True)
for i in range(4):
mag = np.abs(iq_list_selective[i].dot([1, 1j]))
axes[1].plot(t, mag, label=f"Tone {i}")
axes[1].set_ylabel("Magnitude")
axes[1].set_xlabel("Time (µs)")
axes[1].legend()
axes[1].set_title("Individual Tone Responses")
axes[1].grid(True)
plt.tight_layout()
INFO [tproc.py:515] PROCESSOR_STOP
INFO [tproc.py:515] PROCESSOR_STOP
Running selective tone playback...
INFO [tproc.py:662] tProc pmem: loading data
INFO [tproc.py:695] tProc pmem: readback OK
INFO [tproc.py:662] tProc wmem: loading data
INFO [tproc.py:695] tProc wmem: readback OK
INFO [tproc.py:524] PROCESSOR_RESET
INFO [tproc.py:515] PROCESSOR_STOP
INFO [tproc.py:512] PROCESSOR_START
INFO [tproc.py:515] PROCESSOR_STOP
4. Interpolated Generator and Standard Readout
The interpolated generator uses a digital mixer to upconvert an IF signal.
Frequency Matching in tProc v2 vs. tProc v1
Aspect |
tProc v1 |
tProc v2 |
|---|---|---|
Frequency specification |
IF (intermediate frequency) |
Absolute frequency |
Mixer frequency handling |
User must add to both gen and readout |
Automatic |
Common point of confusion |
Forgetting to account for mixer |
None (automatic) |
In tProc v2, you specify the absolute frequency you want to play. The software automatically calculates the correct IF by subtracting the mixer frequency. The readout frequency is also absolute.
[58]:
class SimpleSweepProgram(AveragerProgramV2):
"""
Program demonstrating interpolated generator with parameter sweeps.
Sweeps phase and gain simultaneously across multiple steps.
"""
def _initialize(self, cfg):
ro_ch = cfg['ro_ch']
gen_ch = cfg['gen_ch']
# Declare an interpolated generator with a mixer frequency
self.declare_gen(ch=gen_ch, nqz=1, ro_ch=ro_ch, mixer_freq=cfg['mixer_freq'])
# Declare a standard readout (frequency is absolute)
self.declare_readout(ch=ro_ch, length=cfg['ro_len'], freq=cfg['freq'], gen_ch=gen_ch)
# Add a loop for sweeping parameters
self.add_loop("myloop", self.cfg["steps"])
# Create a flat-top pulse with Gaussian ramps
self.add_gauss(ch=gen_ch, name="ramp", sigma=cfg['ramp_len']/10,
length=cfg['ramp_len'], even_length=True)
self.add_pulse(ch=gen_ch, name="mypulse", ro_ch=ro_ch,
style="flat_top", envelope="ramp",
freq=cfg['freq'], length=cfg['flat_len'],
phase=cfg['phase'], gain=cfg['gain'])
def _body(self, cfg):
self.pulse(ch=cfg['gen_ch'], name="mypulse", t=0)
self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'])
# Configuration
config_sweep = {
'steps': 5,
'gen_ch': INTGEN_CH[0],
'ro_ch': STDRO_CH,
'freq': 2000, # Absolute frequency (MHz)
'mixer_freq': 1500, # Mixer frequency (MHz)
'trig_time': 0.45, # µs
'ro_len': 0.3, # µs
'flat_len': 0.05, # µs
'ramp_len': 0.2, # µs (total pulse length is flat_len + ramp_len)
'phase': QickSweep1D("myloop", -360, 720), # Sweep phase from -360° to 720°
'gain': QickSweep1D("myloop", 0.0, 1.0) # Sweep gain from 0 to 1
}
print("Running interpolated generator with parameter sweeps...")
prog = SimpleSweepProgram(soccfg, reps=1, final_delay=0.5, cfg=config_sweep)
iq_list = prog.acquire_decimated(soc, rounds=1)
t = prog.get_time_axis(ro_index=0)
# Plot results
plt.figure(figsize=(12, 6))
for ii, iq in enumerate(iq_list[0]):
plt.plot(t, iq[:, 0], label=f"I value, step {ii}")
plt.legend()
plt.ylabel("Amplitude")
plt.xlabel("Time (µs)")
plt.title("Swept Phase and Gain with Interpolated Generator")
plt.grid(True)
plt.show()
INFO [tproc.py:515] PROCESSOR_STOP
INFO [tproc.py:515] PROCESSOR_STOP
Running interpolated generator with parameter sweeps...
INFO [tproc.py:662] tProc pmem: loading data
INFO [tproc.py:695] tProc pmem: readback OK
INFO [tproc.py:662] tProc wmem: loading data
INFO [tproc.py:695] tProc wmem: readback OK
INFO [tproc.py:524] PROCESSOR_RESET
INFO [tproc.py:515] PROCESSOR_STOP
INFO [tproc.py:512] PROCESSOR_START
INFO [tproc.py:515] PROCESSOR_STOP
5. Full-Speed Generator and Dynamically Configured Readout
Dynamically configured readouts allow you to change parameters (like frequency) during a sweep. This is essential for resonator spectroscopy and electrical delay calibration.
Key steps:
Define a readout configuration using
add_readoutconfig()Send the configuration using
send_readoutconfig()before it takes effectFor sweeps, send the new configuration in the
exec_beforeblock of the loop
[54]:
class DynamicFreqSweepProgram(AveragerProgramV2):
"""
Program demonstrating dynamic readout configuration for frequency sweeps.
Used for electrical delay calibration and resonator characterization.
"""
def _initialize(self, cfg):
ro_ch = cfg['ro_ch']
gen_ch = cfg['gen_ch']
self.declare_gen(ch=gen_ch, nqz=cfg['nqz'])
self.declare_readout(ch=ro_ch, length=cfg['ro_len'])
# Define the readout configuration (like a pulse for the readout)
self.add_readoutconfig(ch=ro_ch, name="myro", freq=cfg['freq'], gen_ch=gen_ch)
# The exec_before block runs before each loop iteration
# We use it to send the updated readout config
loopbefore = AsmV2()
loopbefore.send_readoutconfig(ch=cfg['ro_ch'], name="myro", t=0)
self.add_loop("myloop", self.cfg["steps"], exec_before=loopbefore)
self.add_gauss(ch=gen_ch, name="ramp", sigma=cfg['ramp_len']/10,
length=cfg['ramp_len'], even_length=True)
self.add_pulse(ch=gen_ch, name="mypulse", ro_ch=ro_ch,
style="flat_top", envelope="ramp",
freq=cfg['freq'], length=cfg['flat_len'],
phase=cfg['phase'], gain=cfg['gain'])
def _body(self, cfg):
self.pulse(ch=cfg['gen_ch'], name="mypulse", t=0)
self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'])
# Small frequency sweep to see phase rotation
START_FREQ = 100
CAL_RANGE = 1
config_dyn = {
'steps': 5,
'gen_ch': FSGEN_CH,
'ro_ch': DYNRO_CH,
'freq': QickSweep1D("myloop", START_FREQ, START_FREQ + CAL_RANGE),
'nqz': 1,
'trig_time': 0.4, # µs
'ro_len': 0.3, # µs
'flat_len': 0.05, # µs
'ramp_len': 0.2, # µs
'phase': 0,
'gain': 1.0
}
print("Running dynamic frequency sweep...")
prog = DynamicFreqSweepProgram(soccfg, reps=10, final_delay=0.5, cfg=config_dyn)
iq_list = prog.acquire(soc, rounds=1)
freqs = prog.get_pulse_param('myro', 'freq', as_array=True)
# The IQ vector rotates as frequency changes due to electrical delay
plt.figure(figsize=(8, 8))
plt.plot(iq_list[0][0][:, 0], iq_list[0][0][:, 1], '.-', markersize=10)
plt.ylabel("Q")
plt.xlabel("I")
plt.title("Phase Rotation from Small Frequency Sweep")
plt.axis('equal')
plt.grid(True)
plt.show()
INFO [tproc.py:515] PROCESSOR_STOP
INFO [tproc.py:515] PROCESSOR_STOP
Running dynamic frequency sweep...
INFO [tproc.py:662] tProc pmem: loading data
INFO [tproc.py:695] tProc pmem: readback OK
INFO [tproc.py:662] tProc wmem: loading data
INFO [tproc.py:695] tProc wmem: readback OK
INFO [tproc.py:524] PROCESSOR_RESET
INFO [tproc.py:515] PROCESSOR_STOP
INFO [tproc.py:512] PROCESSOR_START
INFO [tproc.py:515] PROCESSOR_STOP
5.1 Electrical Delay Calibration
The phase rotation we observe is due to the electrical delay between the DAC and ADC. By measuring the slope of phase vs. frequency, we can calculate this delay and correct for it.
[55]:
# Perform a wide-range sweep for calibration
END_FREQ = 8000 # MHz
config_dyn['freq'] = QickSweep1D("myloop", START_FREQ, END_FREQ)
config_dyn['steps'] = 1001 # Number of frequency points
print("Performing frequency sweep for delay calibration...")
prog = DynamicFreqSweepProgram(soccfg, reps=100, final_delay=1.0, cfg=config_dyn)
freqs = prog.get_pulse_param('myro', 'freq', as_array=True)
# Acquire data with progress bar
start_time = time.time()
iq_list = prog.acquire(soc, rounds=1, progress=True)
iq_complex = iq_list[0][0].dot([1, 1j])
print(f"Sweep completed in {time.time() - start_time:.2f} seconds")
# Measure the phase slope (electrical delay)
phases = np.unwrap(np.angle(iq_complex)) / (2 * np.pi)
a = np.vstack([freqs, np.ones_like(freqs)]).T
phase_delay = np.linalg.lstsq(a, phases, rcond=None)[0][0]
print(f"Measured electrical delay: {phase_delay:.6f} µs")
print(f"This corresponds to {phase_delay * 1e3:.2f} ns of cable delay")
# Apply the correction
iq_rotated = iq_complex * np.exp(-1j * freqs * 2 * np.pi * phase_delay)
# Plot the corrected IQ trajectory
plt.figure(figsize=(8, 8))
plt.plot(np.real(iq_rotated), np.imag(iq_rotated), '.')
plt.ylabel("Q")
plt.xlabel("I")
plt.title("Corrected IQ Trajectory (Flat Phase)")
plt.axis('equal')
plt.grid(True)
plt.show()
# Plot amplitude and phase of the corrected data
phases_corrected = np.unwrap(np.angle(iq_rotated)) / (2 * np.pi)
fig, ax1 = plt.subplots(figsize=(12, 6))
ax1.semilogy(freqs, np.abs(iq_rotated), label="Amplitude", color='blue')
ax1.set_ylabel("Amplitude", color='blue')
ax1.set_xlabel("Frequency (MHz)")
ax1.tick_params(axis='y', labelcolor='blue')
ax1.grid(True)
ax2 = ax1.twinx()
ax2.plot(freqs, phases_corrected, color='red', label='Phase')
ax2.set_ylabel("Phase (cycles)", color='red')
ax2.tick_params(axis='y', labelcolor='red')
plt.title("Frequency Response After Delay Calibration")
fig.tight_layout()
plt.show()
INFO [tproc.py:515] PROCESSOR_STOP
INFO [tproc.py:515] PROCESSOR_STOP
INFO [tproc.py:662] tProc pmem: loading data
INFO [tproc.py:695] tProc pmem: readback OK
INFO [tproc.py:662] tProc wmem: loading data
INFO [tproc.py:695] tProc wmem: readback OK
INFO [tproc.py:524] PROCESSOR_RESET
INFO [tproc.py:515] PROCESSOR_STOP
Performing frequency sweep for delay calibration...
INFO [tproc.py:512] PROCESSOR_START
INFO [tproc.py:515] PROCESSOR_STOP
Sweep completed in 0.37 seconds
Measured electrical delay: -0.011075 µs
This corresponds to -11.08 ns of cable delay
6. Fast Dynamic Readout (Phase-Reset Applications)
The fast dynamic readout (axis_readout_v3) is optimized for applications requiring rapid phase resets between measurements, such as qubit readout in quantum computing experiments. It allows changing readout parameters on every trigger without reconfiguring the entire readout chain.
[81]:
class FastDynamicReadoutProgram(AveragerProgramV2):
"""
Program demonstrating fast dynamic readout for rapid parameter changes.
Ideal for phase-reset applications and fast sequencing.
"""
def _initialize(self, cfg):
ro_ch = cfg['ro_ch']
gen_ch = cfg['gen_ch']
self.declare_gen(ch=gen_ch, nqz=cfg['nqz'])
# declare_readout: length only, no sel/outsel/freq/phase
self.declare_readout(ch=ro_ch, length=cfg['ro_len'])
# outsel goes here in add_readoutconfig only
for i, (freq, phase) in enumerate(zip(cfg['freqs'], cfg['phases'])):
self.add_readoutconfig(
ch=ro_ch,
name=f"ro_conf_{i}",
freq=freq,
phase=phase,
gen_ch=gen_ch,
outsel='input'
)
self.add_gauss(ch=gen_ch, name="ramp", sigma=cfg['ramp_len']/10,
length=cfg['ramp_len'], even_length=True)
for i, (freq, phase) in enumerate(zip(cfg['freqs'], cfg['phases'])):
self.add_pulse(ch=gen_ch,
name=f"pulse_{i}",
ro_ch=ro_ch,
style="flat_top",
envelope="ramp",
freq=freq,
phase=phase,
length=cfg['flat_len'],
gain=cfg['gain']
)
def _body(self, cfg):
t = 0 # running absolute time cursor
step_len = cfg['ro_len'] + 0.1 # readout window + small guard gap
for i in range(cfg['num_steps']):
self.send_readoutconfig(ch=cfg['ro_ch'], name=f"ro_conf_{i}", t=t)
self.pulse(ch=cfg['gen_ch'], name=f"pulse_{i}", t=t)
self.trigger(ros=[cfg['ro_ch']], pins=[0], t=t + cfg['trig_time'])
# Advance time cursor past this step's readout window
self.delay_auto(t=0.1, tag=f"step_{i}") # let QICK resolve end of step
t += step_len # manually track for next pulse/trigger offset
# Example: Fast frequency hopping
config_fast = {
'gen_ch': FSGEN_CH,
'ro_ch': FASTDYNRO_CH,
'nqz': 1,
'freqs': [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000],
'phases': [0] * 10,
'gain': 0.5,
'trig_time': 0.4,
'ro_len': 5.0,
'flat_len': 4.5,
'ramp_len': 0.2,
'num_steps': 10,
'final_delay': 0.5, # per-step gap, passed to delay_auto
}
print("Running fast dynamic readout test (frequency hopping)...")
prog_fast = FastDynamicReadoutProgram(soccfg, reps=100, final_delay=0.5, cfg=config_fast)
iq_list_fast = prog_fast.acquire(soc, rounds=1, progress=True)
iq_array = iq_list_fast[0] # shape (10, 2)
magnitudes = np.abs(iq_array[:, 0] + 1j * iq_array[:, 1])
# Plot results for each frequency
plt.figure(figsize=(8, 5))
plt.plot(config_fast['freqs'], magnitudes, 'o-')
plt.xlabel("Frequency (MHz)")
plt.ylabel("Magnitude (averaged IQ)")
plt.title("Fast Dynamic Readout - Frequency Hopping")
plt.grid(True)
plt.tight_layout()
plt.show()
INFO [tproc.py:515] PROCESSOR_STOP
INFO [tproc.py:515] PROCESSOR_STOP
INFO [tproc.py:662] tProc pmem: loading data
INFO [tproc.py:695] tProc pmem: readback OK
INFO [tproc.py:662] tProc wmem: loading data
INFO [tproc.py:695] tProc wmem: readback OK
INFO [tproc.py:524] PROCESSOR_RESET
INFO [tproc.py:515] PROCESSOR_STOP
Running fast dynamic readout test (frequency hopping)...
INFO [tproc.py:512] PROCESSOR_START
INFO [tproc.py:515] PROCESSOR_STOP
7. Measuring IQ Offsets
Some readout blocks (especially muxed ones) introduce a small, systematic DC offset. The acquire() and acquire_decimated() methods automatically subtract these offsets from the returned data. The raw data (without subtraction) can be accessed via get_raw().
[86]:
class TestProgram(AveragerProgramV2):
"""Simple program to measure IQ offsets across multiple readout channels."""
def _initialize(self, cfg):
for ch in cfg['ro_chs']:
self.declare_readout(ch=ch, length=cfg['ro_len'], freq=100)
def _body(self, cfg):
self.trigger(ros=cfg['ro_chs'], t=0.0)
config_offsets = {
'ro_chs': MUXRO_CH + [STDRO_CH],
'ro_len': 100.0, # µs, long readout to average noise
}
print("Measuring IQ offsets...")
prog = TestProgram(soccfg, reps=1000, final_delay=0.5, cfg=config_offsets)
# Acquire data (offsets are automatically subtracted)
iq_list = prog.acquire(soc, rounds=1)
# Get raw data (to see the offsets)
iq_raw = prog.get_raw()
nch = len(prog.ro_chs)
fig, axes = plt.subplots(2 * nch, 1, figsize=(12, 3 * nch))
for i, (ch, rocfg) in enumerate(prog.ro_chs.items()):
offset = soccfg['readouts'][ch]['iq_offset']
# Check what iq_offset actually looks like
print(f"ch {ch} iq_offset: {offset}, type: {type(offset)}")
# Handle both float and list formats
if isinstance(offset, (list, tuple, np.ndarray)):
offset_i, offset_q = offset[0], offset[1]
else:
offset_i = offset_q = offset # single value applies to both
bins_i = np.linspace(offset_i - 0.1, offset_i + 0.1, 101)
bins_q = np.linspace(offset_q - 0.1, offset_q + 0.1, 101)
nsamp = rocfg['length']
axes[2 * i].hist(iq_raw[i][:, 0, 0] / nsamp, bins=bins_i, alpha=0.7)
axes[2 * i].axvline(offset_i, color='red', linestyle='--',
label=f'Hardware offset: {offset_i:.3f}')
axes[2 * i].set_title(f"Channel {ch} - I Offset")
axes[2 * i].legend()
axes[2 * i + 1].hist(iq_raw[i][:, 0, 1] / nsamp, bins=bins_q, alpha=0.7)
axes[2 * i + 1].axvline(offset_q, color='red', linestyle='--',
label=f'Hardware offset: {offset_q:.3f}')
axes[2 * i + 1].set_title(f"Channel {ch} - Q Offset")
axes[2 * i + 1].legend()
plt.tight_layout()
print("\nNote: The software automatically subtracts these hardware offsets from acquired data.")
print("You can manually access raw data via prog.get_raw() if needed.")
INFO [tproc.py:515] PROCESSOR_STOP
INFO [tproc.py:515] PROCESSOR_STOP
INFO [tproc.py:662] tProc pmem: loading data
INFO [tproc.py:695] tProc pmem: readback OK
INFO [tproc.py:524] PROCESSOR_RESET
INFO [tproc.py:515] PROCESSOR_STOP
Measuring IQ offsets...
INFO [tproc.py:512] PROCESSOR_START
INFO [tproc.py:515] PROCESSOR_STOP
ch 2 iq_offset: -0.5, type: <class 'float'>
ch 3 iq_offset: -0.5, type: <class 'float'>
ch 4 iq_offset: -0.5, type: <class 'float'>
ch 5 iq_offset: -0.5, type: <class 'float'>
ch 6 iq_offset: 0.0, type: <class 'float'>
Note: The software automatically subtracts these hardware offsets from acquired data.
You can manually access raw data via prog.get_raw() if needed.
8. Performance Benchmarks
Understanding the performance characteristics of different configurations helps optimize your experiments.
[87]:
def benchmark_readout(readout_ch, readout_type, ro_len=1.0, reps=1000):
"""Benchmark different readout configurations."""
class BenchProgram(AveragerProgramV2):
def _initialize(self, cfg):
self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'], freq=100)
def _body(self, cfg):
self.trigger(ros=[cfg['ro_ch']], t=0)
cfg = {'ro_ch': readout_ch, 'ro_len': ro_len}
prog = BenchProgram(soccfg, reps=reps, final_delay=0, cfg=cfg)
start = time.time()
prog.acquire(soc, rounds=1)
elapsed = time.time() - start
return elapsed, reps / elapsed # shots per second
print("Benchmarking different readout types...")
print("-" * 60)
benchmarks = []
for ro_ch, name in [(STDRO_CH, "Standard"), (DYNRO_CH, "Dynamic"), (FASTDYNRO_CH, "Fast Dynamic")]:
try:
elapsed, rate = benchmark_readout(ro_ch, name)
benchmarks.append((name, elapsed, rate))
print(f"{name:15} Readout: {elapsed:.3f}s for 1000 shots ({rate:.0f} shots/sec)")
except Exception as e:
print(f"{name:15} Readout: Error - {e}")
# Benchmark muxed readout (multiple channels)
class BenchMuxProgram(AveragerProgramV2):
def _initialize(self, cfg):
for ch in cfg['ro_chs']:
self.declare_readout(ch=ch, length=cfg['ro_len'], freq=100)
def _body(self, cfg):
self.trigger(ros=cfg['ro_chs'], t=0)
cfg_mux = {'ro_chs': MUXRO_CH, 'ro_len': 1.0}
prog_mux = BenchMuxProgram(soccfg, reps=1000, final_delay=0, cfg=cfg_mux)
start = time.time()
prog_mux.acquire(soc, rounds=1)
elapsed_mux = time.time() - start
print(f"{'Muxed (4 ch)':15} Readout: {elapsed_mux:.3f}s for 1000 shots ({1000/elapsed_mux:.0f} shots/sec)")
print("\nPerformance Notes:")
print("• Fast Dynamic readout is optimized for rapid phase resets")
print("• Muxed readout processes multiple channels simultaneously")
print("• Longer readout lengths proportionally increase acquisition time")
INFO [tproc.py:515] PROCESSOR_STOP
INFO [tproc.py:515] PROCESSOR_STOP
INFO [tproc.py:662] tProc pmem: loading data
INFO [tproc.py:695] tProc pmem: readback OK
INFO [tproc.py:524] PROCESSOR_RESET
INFO [tproc.py:515] PROCESSOR_STOP
Benchmarking different readout types...
------------------------------------------------------------
INFO [tproc.py:512] PROCESSOR_START
INFO [tproc.py:515] PROCESSOR_STOP
INFO [tproc.py:515] PROCESSOR_STOP
INFO [tproc.py:515] PROCESSOR_STOP
Standard Readout: 0.175s for 1000 shots (5720 shots/sec)
Dynamic Readout: Error - readout 0 is dynamic (tProc-configured) - freq/phase/sel parameters are set using tProc instructions, not in declaration
Fast Dynamic Readout: Error - readout 1 is dynamic (tProc-configured) - freq/phase/sel parameters are set using tProc instructions, not in declaration
INFO [tproc.py:662] tProc pmem: loading data
INFO [tproc.py:695] tProc pmem: readback OK
INFO [tproc.py:524] PROCESSOR_RESET
INFO [tproc.py:515] PROCESSOR_STOP
INFO [tproc.py:512] PROCESSOR_START
INFO [tproc.py:515] PROCESSOR_STOP
Muxed (4 ch) Readout: 0.192s for 1000 shots (5207 shots/sec)
Performance Notes:
• Fast Dynamic readout is optimized for rapid phase resets
• Muxed readout processes multiple channels simultaneously
• Longer readout lengths proportionally increase acquisition time
9. Real-Time Phase Tracking Example
For qubit stability measurements, tracking phase over multiple shots is crucial.
[94]:
class PhaseTrackingProgram(AveragerProgramV2):
"""
Program for tracking shot-to-shot phase stability.
Useful for qubit stability measurements and noise characterization.
"""
def _initialize(self, cfg):
ro_ch = cfg['ro_ch']
gen_ch = cfg['gen_ch']
self.declare_gen(ch=gen_ch, nqz=1)
self.declare_readout(ch=ro_ch, length=cfg['ro_len'], freq=cfg['freq'], gen_ch=gen_ch)
self.add_pulse(ch=gen_ch, name="probe", style="const",
length=cfg['pulse_len'], freq=cfg['freq'], ro_ch=ro_ch,
phase=cfg['phase'], gain=cfg['gain'])
def _body(self, cfg):
self.trigger(ros=[cfg['ro_ch']], t=cfg['trig_time'])
self.pulse(ch=cfg['gen_ch'], name="probe", t=0)
# Track phase over many shots
# Phase tracking using get_raw() to recover per-rep IQ data
n_shots = 500
config_tracking = {
'gen_ch': FSGEN_CH,
'ro_ch': STDRO_CH,
'freq': 500,
'gain': 0.3,
'phase': 0,
'trig_time': 0.4,
'ro_len': 0.5,
'pulse_len': 0.3,
}
print(f"Tracking phase over {n_shots} shots...")
prog_track = PhaseTrackingProgram(soccfg, reps=n_shots, final_delay=0.1, cfg=config_tracking)
iq_list = prog_track.acquire(soc, rounds=1)
iq_raw = prog_track.get_raw()
# Extract per-rep IQ points
# iq_raw shape: (n_ro_channels, n_reps, n_trigs, 2)
nsamp = prog_track.ro_chs[STDRO_CH]['length']
iq_shots = iq_raw[0][:, 0, :] # (n_reps, 2)
i_vals = iq_shots[:, 0] / nsamp
q_vals = iq_shots[:, 1] / nsamp
phases_shots = np.angle(i_vals + 1j * q_vals, deg=True)
print(f"n_shots recovered: {len(phases_shots)}")
print(f"Mean I: {np.mean(i_vals):.4f}")
print(f"Mean Q: {np.mean(q_vals):.4f}")
print(f"Mean magnitude: {np.mean(np.abs(i_vals + 1j*q_vals)):.4f}")
print(f"Std magnitude: {np.std( np.abs(i_vals + 1j*q_vals)):.4f}")
print(f"Phase stability: {np.std(phases_shots):.3f} degrees RMS")
# ── Diagnostics ────────────────────────────────────────────────────────────────
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# 1. IQ cloud
axes[0, 0].scatter(i_vals, q_vals, alpha=0.3, s=10)
axes[0, 0].axhline(0, color='k', linewidth=0.5)
axes[0, 0].axvline(0, color='k', linewidth=0.5)
axes[0, 0].set_xlabel("I")
axes[0, 0].set_ylabel("Q")
axes[0, 0].set_title("IQ Cloud (tight cluster = good signal)")
axes[0, 0].axis('equal')
axes[0, 0].grid(True)
# 2. Per-shot magnitude
magnitudes = np.abs(i_vals + 1j * q_vals)
axes[0, 1].plot(magnitudes, '.-', alpha=0.5, markersize=3)
axes[0, 1].set_xlabel("Shot")
axes[0, 1].set_ylabel("Magnitude")
axes[0, 1].set_title("Per-shot Magnitude (stable & non-zero = signal present)")
axes[0, 1].axhline(np.mean(magnitudes), color='red', label=f"Mean: {np.mean(magnitudes):.4f}")
axes[0, 1].legend()
axes[0, 1].grid(True)
# 3. Phase vs shot
axes[1, 0].plot(phases_shots, '.-', alpha=0.5, markersize=3)
axes[1, 0].set_ylabel("Phase (degrees)")
axes[1, 0].set_xlabel("Shot Number")
axes[1, 0].set_title("Shot-to-Shot Phase Stability")
axes[1, 0].axhline(np.mean(phases_shots), color='red',
label=f"Mean: {np.mean(phases_shots):.1f}°")
axes[1, 0].axhline(np.mean(phases_shots) + np.std(phases_shots), color='orange',
linestyle='--', label=f"±1σ: {np.std(phases_shots):.2f}°")
axes[1, 0].axhline(np.mean(phases_shots) - np.std(phases_shots), color='orange',
linestyle='--')
axes[1, 0].legend()
axes[1, 0].grid(True)
# 4. Phase histogram
axes[1, 1].hist(phases_shots, bins=50, alpha=0.7)
axes[1, 1].set_xlabel("Phase (degrees)")
axes[1, 1].set_ylabel("Counts")
axes[1, 1].set_title(f"Phase Distribution (σ = {np.std(phases_shots):.3f}°)")
axes[1, 1].grid(True)
plt.tight_layout()
plt.show()
# ── Interpretation ─────────────────────────────────────────────────────────────
mag_mean = np.mean(magnitudes)
mag_std = np.std(magnitudes)
phase_std = np.std(phases_shots)
print("\n── Signal diagnosis ──────────────────────────────────────────────────")
if mag_mean < 0.01:
print("❌ Magnitude near zero — no signal reaching ADC.")
print(" → Check wiring, increase gain, verify frequency is on resonance.")
elif mag_std / mag_mean > 0.5:
print("⚠️ Magnitude highly variable — trigger may be firing before pulse arrives.")
print(" → Swap pulse/trigger order in _body, or increase trig_time.")
elif phase_std > 10:
print("⚠️ Large phase scatter despite signal present.")
print(" → Frequency may be off resonance, or DDS phase not reset between reps.")
print(" → Try adding phrst=1 to the pulse, or scan freq to find resonance.")
else:
print(f"✅ Good signal. Phase stability: {phase_std:.3f}° RMS")
INFO [tproc.py:515] PROCESSOR_STOP
INFO [tproc.py:515] PROCESSOR_STOP
INFO [tproc.py:662] tProc pmem: loading data
INFO [tproc.py:695] tProc pmem: readback OK
INFO [tproc.py:662] tProc wmem: loading data
INFO [tproc.py:695] tProc wmem: readback OK
INFO [tproc.py:524] PROCESSOR_RESET
INFO [tproc.py:515] PROCESSOR_STOP
Tracking phase over 500 shots...
INFO [tproc.py:512] PROCESSOR_START
INFO [tproc.py:515] PROCESSOR_STOP
n_shots recovered: 500
Mean I: 0.0019
Mean Q: -0.0018
Mean magnitude: 0.1416
Std magnitude: 0.0760
Phase stability: 101.925 degrees RMS
── Signal diagnosis ──────────────────────────────────────────────────
⚠️ Magnitude highly variable — trigger may be firing before pulse arrives.
→ Swap pulse/trigger order in _body, or increase trig_time.
10. Common Pitfalls and Troubleshooting
This section covers frequently encountered issues and their solutions.
[32]:
print("""
╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
║ COMMON PITFALLS AND SOLUTIONS ║
╠══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╣
║ ║
║ Issue │ Likely Cause │ Solution ║
║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║
║ No signal in readout │ Wrong physical connection │ Check DAC→ADC loopback cables ║
║ │ Generator not enabled │ Verify gen_ch is correct ║
║ │ Wrong nqz setting │ nqz=1 for Nyquist zone 1 ║
║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║
║ Phase rotates with frequency │ Electrical delay not calibrated │ Run calibration sweep (see Section 5) ║
║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║
║ Muxed readout wrong phases │ No frequency matching │ Set gen_ch in declare_readout() ║
║ │ Tone order mismatch │ Ensure frequencies match order ║
║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║
║ Interpolated gen freq off │ Forgot mixer_freq in tProc v2 │ Use absolute frequencies, not IFs ║
║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║
║ PFB channels swapped │ Tone order mismatch │ Verify pulse_freqs order matches ro_chs ║
║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║
║ Acquisition too slow │ Too many reps without averaging │ Use software averaging or reduce reps ║
║ │ Long final_delay │ Minimize final_delay when possible ║
║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║
║ Distorted pulse shape │ Pulse too short for ramp │ Ensure flat_len >= ramp_len/2 ║
║ │ DAC saturation │ Reduce gain ║
║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║
║ Memory errors │ Too many pulses/readouts │ Use loops and dynamic configs ║
║ │ Pulse/readout length too long │ Limit to < 2^15 samples ║
║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║
║ Fast dynamic readout issues │ Wrong channel type │ Use FASTDYNRO_CH, not DYNRO_CH ║
║ │ Missing send_readoutconfig │ Send config before each trigger ║
║ ║
╚══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
""")
╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
║ COMMON PITFALLS AND SOLUTIONS ║
╠══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╣
║ ║
║ Issue │ Likely Cause │ Solution ║
║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║
║ No signal in readout │ Wrong physical connection │ Check DAC→ADC loopback cables ║
║ │ Generator not enabled │ Verify gen_ch is correct ║
║ │ Wrong nqz setting │ nqz=1 for Nyquist zone 1 ║
║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║
║ Phase rotates with frequency │ Electrical delay not calibrated │ Run calibration sweep (see Section 5) ║
║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║
║ Muxed readout wrong phases │ No frequency matching │ Set gen_ch in declare_readout() ║
║ │ Tone order mismatch │ Ensure frequencies match order ║
║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║
║ Interpolated gen freq off │ Forgot mixer_freq in tProc v2 │ Use absolute frequencies, not IFs ║
║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║
║ PFB channels swapped │ Tone order mismatch │ Verify pulse_freqs order matches ro_chs ║
║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║
║ Acquisition too slow │ Too many reps without averaging │ Use software averaging or reduce reps ║
║ │ Long final_delay │ Minimize final_delay when possible ║
║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║
║ Distorted pulse shape │ Pulse too short for ramp │ Ensure flat_len >= ramp_len/2 ║
║ │ DAC saturation │ Reduce gain ║
║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║
║ Memory errors │ Too many pulses/readouts │ Use loops and dynamic configs ║
║ │ Pulse/readout length too long │ Limit to < 2^15 samples ║
║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║
║ Fast dynamic readout issues │ Wrong channel type │ Use FASTDYNRO_CH, not DYNRO_CH ║
║ │ Missing send_readoutconfig │ Send config before each trigger ║
║ ║
╚══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
11. Summary and Best Practices
[33]:
print("""
╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
║ BEST PRACTICES SUMMARY ║
╠══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╣
║ ║
║ ✓ Always calibrate electrical delay before precision phase measurements ║
║ ✓ Use muxed generators + PFB readout for multi-tone experiments ║
║ ✓ In tProc v2, specify ABSOLUTE frequencies (not IFs) ║
║ ✓ Link generators to readouts with gen_ch parameter for automatic frequency matching ║
║ ✓ Use dynamic readouts for frequency sweeps to save configuration time ║
║ ✓ Keep pulse lengths within BRAM constraints (< 2^15 samples @ 1ns sample rate = 32.768 µs) ║
║ ✓ Use software averaging (reps parameter) for noise reduction ║
║ ✓ Monitor IQ offsets and apply corrections when necessary ║
║ ✓ Test new configurations with low power and short pulses first ║
║ ✓ Use progress=True for long acquisitions to monitor status ║
║ ║
╠══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╣
║ WHEN TO USE EACH TOOL ║
╠══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╣
║ ║
║ • Full-Speed Generator: Single tone, maximum bandwidth, simple pulses ║
║ • Interpolated Generator: Fine frequency resolution, shaped pulses ║
║ • Muxed Generators: Multiple phase-coherent tones ║
║ • Standard Readout: Fixed-frequency averaging ║
║ • Dynamic Readout: Frequency sweeps, calibration ║
║ • Fast Dynamic Readout: Phase-reset applications, rapid sequencing ║
║ • PFB Readout: Multi-tone demodulation with 64 channels ║
║ ║
╚══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
""")
╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
║ BEST PRACTICES SUMMARY ║
╠══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╣
║ ║
║ ✓ Always calibrate electrical delay before precision phase measurements ║
║ ✓ Use muxed generators + PFB readout for multi-tone experiments ║
║ ✓ In tProc v2, specify ABSOLUTE frequencies (not IFs) ║
║ ✓ Link generators to readouts with gen_ch parameter for automatic frequency matching ║
║ ✓ Use dynamic readouts for frequency sweeps to save configuration time ║
║ ✓ Keep pulse lengths within BRAM constraints (< 2^15 samples @ 1ns sample rate = 32.768 µs) ║
║ ✓ Use software averaging (reps parameter) for noise reduction ║
║ ✓ Monitor IQ offsets and apply corrections when necessary ║
║ ✓ Test new configurations with low power and short pulses first ║
║ ✓ Use progress=True for long acquisitions to monitor status ║
║ ║
╠══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╣
║ WHEN TO USE EACH TOOL ║
╠══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╣
║ ║
║ • Full-Speed Generator: Single tone, maximum bandwidth, simple pulses ║
║ • Interpolated Generator: Fine frequency resolution, shaped pulses ║
║ • Muxed Generators: Multiple phase-coherent tones ║
║ • Standard Readout: Fixed-frequency averaging ║
║ • Dynamic Readout: Frequency sweeps, calibration ║
║ • Fast Dynamic Readout: Phase-reset applications, rapid sequencing ║
║ • PFB Readout: Multi-tone demodulation with 64 channels ║
║ ║
╚══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
12. Next Steps
You have now explored the key advanced generator and readout blocks in tProc v2.
Proceed to `08_Hardware_Buffers.ipynb <./08_Hardware_Buffers.ipynb>`__ to learn about the DDR4 and MR buffers for high-speed data capture.
Key Takeaways:
tProc v2 uses absolute frequencies - no more manual IF calculations
Link generators and readouts with
gen_chfor automatic frequency matchingCalibrate electrical delay for accurate phase measurements
Choose the right tool based on your application requirements
Monitor hardware offsets and apply corrections as needed
Additional Resources:
To learn about basic sequencing, proceed to
`01_Basic_Sequencing.ipynb<./01_Basic_Sequencing.ipynb>`__For parameter sweeps, see
`02_Parameter_Sweeps.ipynb<./02_Parameter_Sweeps.ipynb>`__For real-time feedback, see
`04_Real_Time_Feedback.ipynb<./04_Real_Time_Feedback.ipynb>`__For multi-core synchronization, see
`10_Multi_Core_Synchronization.ipynb<./10_Multi_Core_Synchronization.ipynb>`__