07 - Advanced Generators and Readouts in tProc v2

Objective: Explore the advanced generator and readout blocks available in the QICK firmware. This notebook covers:

  1. Muxed Generators (full-speed and mixer-mux) with Muxed Readouts (PFB)

  2. Interpolated Generators vs. Standard Readouts (frequency matching in tProc v2 vs. v1)

  3. Dynamically Configured Readouts for frequency sweeps and electrical delay calibration

  4. Fast Dynamic Readout for phase-reset applications

  5. IQ offset measurement and correction

  6. Performance benchmarks

  7. Real-time phase tracking

  8. Common pitfalls and troubleshooting

  9. 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 mask parameter in add_pulse selects which tones are played

  • The PFB readout provides finer channel spacing than standard muxed readout (64 slices vs 8)

  • Linking generators to readouts with gen_ch enables 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
../_images/tutorials_07_Advanced_Generators_And_Readouts_7_5.png

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
../_images/tutorials_07_Advanced_Generators_And_Readouts_9_7.png

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
../_images/tutorials_07_Advanced_Generators_And_Readouts_11_5.png

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
../_images/tutorials_07_Advanced_Generators_And_Readouts_13_5.png

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
../_images/tutorials_07_Advanced_Generators_And_Readouts_15_5.png

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:

  1. Define a readout configuration using add_readoutconfig()

  2. Send the configuration using send_readoutconfig() before it takes effect

  3. For sweeps, send the new configuration in the exec_before block 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
../_images/tutorials_07_Advanced_Generators_And_Readouts_17_5.png

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
../_images/tutorials_07_Advanced_Generators_And_Readouts_19_5.png
../_images/tutorials_07_Advanced_Generators_And_Readouts_19_6.png

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
../_images/tutorials_07_Advanced_Generators_And_Readouts_21_4.png

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.
../_images/tutorials_07_Advanced_Generators_And_Readouts_23_5.png

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
../_images/tutorials_07_Advanced_Generators_And_Readouts_27_5.png

── 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:

  1. tProc v2 uses absolute frequencies - no more manual IF calculations

  2. Link generators and readouts with gen_ch for automatic frequency matching

  3. Calibrate electrical delay for accurate phase measurements

  4. Choose the right tool based on your application requirements

  5. 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>`__