03 - Advanced Timing with tProc v2
Objective: Understand the critical differences between delay(), wait(), delay_auto(), and wait_auto(). Learn how to manage the timeline correctly to avoid pulse collisions and timing errors.
1. Setup
[1]:
# Standard imports
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import logging
from qick import *
from qick.asm_v2 import AveragerProgramV2, QickSweep1D
# Setup logging
logging.basicConfig(level=logging.INFO, format='%(levelname)-8s [%(filename)s:%(lineno)d] %(message)s')
logging.getLogger("qick_processor").setLevel(logging.WARNING)
# Connect to the board (adjust the path to your firmware)
BITSTREAM_PATH = 'qick_216.bit'#'/path/to/your/firmware.bit' # <--- CHANGE THIS
soc = QickSoc(BITSTREAM_PATH)
soccfg = soc
# Define hardware channels
GEN_CH = 0
RO_CH = 0
2. Understanding the Timeline
In tProc v2, all timed instructions (pulses, triggers, delays) are scheduled on a common timeline. The tProc maintains a reference time that advances as instructions are executed.
Key concepts:
Absolute time: The actual time from the start of the shot
Reference time: The tProc’s internal clock, which determines when the next instruction will execute
Scheduled time: The time you specify for a pulse or trigger (e.g.,
t=0.5)
The four timing commands:
Command |
What it does |
When to use |
|---|---|---|
|
Adds |
When you know exactly how long to wait |
|
Waits until the reference time reaches |
When you need to wait for a specific absolute time |
|
Adds |
Safe way to ensure everything finishes |
|
Waits until reference time reaches |
Similar to v1’s |
3. Basic Timing: delay() vs wait()
The simplest timing commands are delay() and wait(). They work with absolute and relative timing.
[2]:
class TimingExampleProgram(AveragerProgramV2):
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'])
# Define a constant pulse
self.add_pulse(ch=gen_ch, name="test_pulse",
style="const",
freq=cfg['freq'], ro_ch=ro_ch,
length=cfg['pulse_len'],
phase=0, gain=1.0)
self.add_readoutconfig(ch=ro_ch, name="my_ro",
freq=cfg['freq'], gen_ch=gen_ch)
self.send_readoutconfig(ch=ro_ch, name="my_ro", t=0)
def _body(self, cfg):
# Trigger readout at t=0.5 µs
self.trigger(ros=[cfg['ro_ch']], pins=[0], t=0.5)
# First pulse at t=0
self.pulse(ch=cfg['gen_ch'], name="test_pulse", t=0)
if cfg['use_wait']:
# Using wait: pause until reference time reaches 0.8 µs
self.wait(0.8)
else:
# Using delay: add 0.8 µs to reference time
self.delay(0.8)
# Second pulse - when does it play?
self.pulse(ch=cfg['gen_ch'], name="test_pulse", t=0)
# Compare delay vs wait
config = {
'gen_ch': GEN_CH,
'ro_ch': RO_CH,
'freq': 500,
'pulse_len': 0.1,
'ro_len': 2.0,
'use_wait': False
}
print("=== Using DELAY ===")
prog = TimingExampleProgram(soccfg, reps=1, final_delay=0.5, cfg=config)
# Get the compiled program as text to inspect the timing
print(prog.asm())
print("\n=== Using WAIT ===")
config['use_wait'] = True
prog = TimingExampleProgram(soccfg, reps=1, final_delay=0.5, cfg=config)
print(prog.asm())
WARNING [asm_v2.py:998] warning: pulse time 0 appears to conflict with previous pulse ending at <qick.asm_v2.QickParam object at 0xffff8e46de70>?
=== Using DELAY ===
NOP
REG_WR s12 imm #0
WPORT_WR p4 wmem [&1] @0
TIME #430 inc_ref
REG_WR r0 imm #0
reps:
TRIG p0 set @215
TRIG p10 set @215
TRIG p0 clr @225
TRIG p10 clr @225
WPORT_WR p0 wmem [&0] @0
TIME #344 inc_ref
WPORT_WR p0 wmem [&0] @0
REG_WR s15 label SKIP
WAIT [s15] @731 time
TIME #946 inc_ref
REG_WR s12 op -op(s12 + #1)
REG_WR s15 label reps
TEST -op(r0 - #0)
JUMP [s15] -if(NZ) -wr(r0 op) -op(r0 + #1)
REG_WR s15 label NEXT
JUMP [s15]
=== Using WAIT ===
NOP
REG_WR s12 imm #0
WPORT_WR p4 wmem [&1] @0
TIME #430 inc_ref
REG_WR r0 imm #0
reps:
TRIG p0 set @215
TRIG p10 set @215
TRIG p0 clr @225
TRIG p10 clr @225
WPORT_WR p0 wmem [&0] @0
REG_WR s15 label SKIP
WAIT [s15] @344 time
WPORT_WR p0 wmem [&0] @0
REG_WR s15 label SKIP
WAIT [s15] @1075 time
TIME #1290 inc_ref
REG_WR s12 op -op(s12 + #1)
REG_WR s15 label reps
TEST -op(r0 - #0)
JUMP [s15] -if(NZ) -wr(r0 op) -op(r0 + #1)
REG_WR s15 label NEXT
JUMP [s15]
Key difference:
delay(0.8)adds 0.8 µs to the reference time, so the second pulse plays 0.8 µs after the first pulse endswait(0.8)pauses until the reference time reaches exactly 0.8 µs (absolute time from shot start)
Rule of thumb: Use delay() for relative timing between pulses. Use wait() for absolute timing or when waiting for a specific absolute time.
4. Auto-Timing: delay_auto() and wait_auto()
The _auto variants automatically account for pending operations (pulses that haven’t finished, readouts that are still acquiring). This is safer when you’re not sure how long previous operations will take.
[3]:
class AutoTimingProgram(AveragerProgramV2):
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'])
# Define pulses with swept lengths
self.add_pulse(ch=gen_ch, name="pulse_a",
style="const",
freq=cfg['freq'], ro_ch=ro_ch,
length=cfg['len_a'],
phase=0, gain=1.0)
self.add_pulse(ch=gen_ch, name="pulse_b",
style="const",
freq=cfg['freq'], ro_ch=ro_ch,
length=cfg['len_b'],
phase=0, gain=1.0)
self.add_readoutconfig(ch=ro_ch, name="my_ro",
freq=cfg['freq'], gen_ch=gen_ch)
self.send_readoutconfig(ch=ro_ch, name="my_ro", t=0)
def _body(self, cfg):
# Play first pulse at t=0
self.pulse(ch=cfg['gen_ch'], name="pulse_a", t=0)
# Auto-delay: wait for pulse_a to finish, then add extra time
# ros=False means don't wait for readout (which hasn't started yet)
self.delay_auto(cfg['gap'], ros=False)
# Play second pulse
self.pulse(ch=cfg['gen_ch'], name="pulse_b", t=0)
# Trigger readout after both pulses
self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'])
# Demo: pulse lengths vary, but the gap between them stays constant
config_auto = {
'gen_ch': GEN_CH,
'ro_ch': RO_CH,
'freq': 500,
'len_a': 0.2, # µs
'len_b': 0.3, # µs (different length!)
'gap': 0.1, # µs gap between pulses
'trig_time': 0.8, # µs
'ro_len': 1.5 # µs
}
prog = AutoTimingProgram(soccfg, reps=1, final_delay=0.5, cfg=config_auto)
# Print the compiled program to see the generated assembly
print(prog.asm())
NOP
REG_WR s12 imm #0
WPORT_WR p4 wmem [&2] @0
TIME #430 inc_ref
REG_WR r0 imm #0
reps:
WPORT_WR p0 wmem [&0] @0
TIME #129 inc_ref
WPORT_WR p0 wmem [&1] @0
TRIG p0 set @344
TRIG p10 set @344
TRIG p0 clr @354
TRIG p10 clr @354
REG_WR s15 label SKIP
WAIT [s15] @989 time
TIME #1205 inc_ref
REG_WR s12 op -op(s12 + #1)
REG_WR s15 label reps
TEST -op(r0 - #0)
JUMP [s15] -if(NZ) -wr(r0 op) -op(r0 + #1)
REG_WR s15 label NEXT
JUMP [s15]
Why ``delay_auto()`` is powerful:
It automatically calculates when pulse A ends
Then schedules pulse B to start at
(end_of_pulse_A + gap)This works even if pulse lengths change (e.g., in a sweep)
Parameters:
gens=True/False: Whether to wait for generator operations (pulses)ros=True/False: Whether to wait for readout operationsDefault for
delay_auto():gens=True, ros=TrueDefault for
wait_auto():gens=False, ros=True(like v1’ssync_all)
5. Common Pitfall: Pulse Collisions
If you schedule a pulse before the previous one finishes, the tProc will give you a warning (if you have logging enabled) but will still execute. This can cause unexpected results.
[4]:
class CollisionProgram(AveragerProgramV2):
def _initialize(self, cfg):
self.declare_gen(ch=cfg['gen_ch'], nqz=1)
self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'])
self.add_pulse(ch=cfg['gen_ch'], name="long_pulse",
style="const",
freq=cfg['freq'], ro_ch=cfg['ro_ch'], length=0.5, phase=0, gain=1.0)
self.add_readoutconfig(ch=cfg['ro_ch'], name="my_ro",
freq=cfg['freq'], gen_ch=cfg['gen_ch'])
self.send_readoutconfig(ch=cfg['ro_ch'], name="my_ro", t=0)
def _body(self, cfg):
# First pulse at t=0
self.pulse(ch=cfg['gen_ch'], name="long_pulse", t=0)
# This pulse is scheduled while the first is still playing!
# The tProc will issue a warning.
self.pulse(ch=cfg['gen_ch'], name="long_pulse", t=0.3)
self.trigger(ros=[cfg['ro_ch']], pins=[0], t=0.6)
config_collision = {
'gen_ch': GEN_CH,
'ro_ch': RO_CH,
'freq': 500,
'ro_len': 1.0
}
prog = CollisionProgram(soccfg, reps=1, final_delay=0.5, cfg=config_collision)
print("Program with potential pulse collision:")
print(prog.asm())
print("\nNote: The second pulse is scheduled before the first finishes!")
WARNING [asm_v2.py:998] warning: pulse time 0.3 appears to conflict with previous pulse ending at <qick.asm_v2.QickParam object at 0xffff649029e0>?
Program with potential pulse collision:
NOP
REG_WR s12 imm #0
WPORT_WR p4 wmem [&1] @0
TIME #430 inc_ref
REG_WR r0 imm #0
reps:
WPORT_WR p0 wmem [&0] @0
WPORT_WR p0 wmem [&0] @129
TRIG p0 set @258
TRIG p10 set @258
TRIG p0 clr @268
TRIG p10 clr @268
REG_WR s15 label SKIP
WAIT [s15] @688 time
TIME #903 inc_ref
REG_WR s12 op -op(s12 + #1)
REG_WR s15 label reps
TEST -op(r0 - #0)
JUMP [s15] -if(NZ) -wr(r0 op) -op(r0 + #1)
REG_WR s15 label NEXT
JUMP [s15]
Note: The second pulse is scheduled before the first finishes!
How to fix: Use delay_auto() to ensure proper spacing.
# Instead of:
self.pulse(ch=gen_ch, name="long_pulse", t=0)
self.pulse(ch=gen_ch, name="long_pulse", t=0.3) # Collision!
# Do this:
self.pulse(ch=gen_ch, name="long_pulse", t=0)
self.delay_auto(0.1) # Wait for pulse to finish, then add 0.1 µs
self.pulse(ch=gen_ch, name="long_pulse", t=0) # Safely scheduled
6. Sweeping Delays
You can sweep delay times just like pulse parameters.
[5]:
class DelaySweepProgram(AveragerProgramV2):
def _initialize(self, cfg):
self.declare_gen(ch=cfg['gen_ch'], nqz=1)
self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'])
self.add_loop("delay_sweep", self.cfg["steps"])
self.add_pulse(ch=cfg['gen_ch'], name="test_pulse",
style="const",
freq=cfg['freq'], ro_ch=cfg['ro_ch'], length=0.1,
phase=0, gain=1.0)
self.add_readoutconfig(ch=cfg['ro_ch'], name="my_ro",
freq=cfg['freq'], gen_ch=cfg['gen_ch'])
self.send_readoutconfig(ch=cfg['ro_ch'], name="my_ro", t=0)
def _body(self, cfg):
# Trigger readout at fixed time
self.trigger(ros=[cfg['ro_ch']], pins=[0], t=0.5)
# First pulse at t=0
self.pulse(ch=cfg['gen_ch'], name="test_pulse", t=0)
# Sweep the delay between pulses
self.delay(QickSweep1D("delay_sweep", 0.1, 0.5))
# Second pulse
self.pulse(ch=cfg['gen_ch'], name="test_pulse", t=0)
config_delay_sweep = {
'steps': 5,
'gen_ch': GEN_CH,
'ro_ch': RO_CH,
'freq': 500,
'ro_len': 1.0
}
prog = DelaySweepProgram(soccfg, reps=1, final_delay=0.5, cfg=config_delay_sweep)
# Show that the program compiles with swept delays
print("Program with swept delay:")
print(prog.asm())
print("\nNote: The tProc will automatically handle the varying delay times.")
Program with swept delay:
NOP
REG_WR r2 imm #43
REG_WR r3 imm #817
REG_WR s12 imm #0
WPORT_WR p4 wmem [&1] @0
TIME #430 inc_ref
REG_WR r0 imm #0
reps:
REG_WR r1 imm #0
delay_sweep:
TRIG p0 set @215
TRIG p10 set @215
TRIG p0 clr @225
TRIG p10 clr @225
WPORT_WR p0 wmem [&0] @0
TIME inc_ref r2
WPORT_WR p0 wmem [&0] @0
REG_WR s15 label SKIP
WAIT [s15] @602 time
TIME inc_ref r3
REG_WR s12 op -op(s12 + #1)
REG_WR r2 op -op(r2 + #43)
REG_WR r3 op -op(r3 + #-43)
REG_WR s15 label delay_sweep
TEST -op(r1 - #4)
JUMP [s15] -if(NZ) -wr(r1 op) -op(r1 + #1)
REG_WR r2 op -op(r2 + #-215)
REG_WR r3 op -op(r3 + #215)
REG_WR s15 label reps
TEST -op(r0 - #0)
JUMP [s15] -if(NZ) -wr(r0 op) -op(r0 + #1)
REG_WR s15 label NEXT
JUMP [s15]
Note: The tProc will automatically handle the varying delay times.
7. Summary
You have learned:
The difference between
delay()(relative) andwait()(absolute)How
delay_auto()andwait_auto()automatically account for pending operationsHow to avoid pulse collisions by using proper timing commands
How to sweep delay times
How to inspect the generated assembly code using
asm()to verify timing
Best practices:
Use ``delay_auto()`` when you want to wait for pulses to finish - It’s safer and works with swept lengths
Use ``wait()`` for absolute timing - When you need to hit an exact absolute time
Use ``delay()`` for simple relative timing - When you know exactly how long to wait
Always check for pulse collisions - Use
print(prog.asm())to inspect the generated codeUse tags to identify instructions - Tags help with debugging and retrieving parameters
Next steps: Proceed to `04_Real_Time_Feedback.ipynb <./04_Real_Time_Feedback.ipynb>`__ to learn how to read measurement results and make decisions on the tProc in real-time.