01 - Basic Sequencing with tProc v2
Objective: Learn how to define pulses, create a sequence, acquire data, and understand the basic structure of a tProc v2 program.
1. Setup
[5]:
# Standard imports
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import logging
from qick import *
from qick.asm_v2 import AveragerProgramV2
# Setup logging
logging.basicConfig(level=logging.INFO, format='%(levelname)-8s [%(filename)s:%(lineno)d] %(message)s')
logging.getLogger("qick_processor").setLevel(logging.WARNING)
# Connect to the board (adjust the path to your firmware)
BITSTREAM_PATH = 'qick_216.bit'#'/path/to/your/firmware.bit' # <--- CHANGE THIS
soc = QickSoc(BITSTREAM_PATH)
soccfg = soc
# Define hardware channels (adjust based on your firmware)
GEN_CH = 0 # Generator channel 0
RO_CH = 0 # Readout channel 0
2. Your First Program: The Structure
Every tProc v2 program follows this pattern:
[6]:
class MyFirstProgram(AveragerProgramV2):
def _initialize(self, cfg):
"""Setup: Called once when the program is compiled."""
# 1. Declare hardware resources
# 2. Define envelopes and pulses
# 3. Configure readout
pass
def _body(self, cfg):
"""Sequence: Called for each repetition (shot)."""
# 1. Play pulses
# 2. Trigger readout
pass
The key distinction:
``_initialize()`` runs during compilation (Python side, once)
``_body()`` runs during execution (tProc side, many times)
3. A Simple Single-Pulse Program
Let’s create a program that plays a single constant pulse and measures it.
[7]:
class SinglePulseProgram(AveragerProgramV2):
def _initialize(self, cfg):
# Declare the generator and readout channels
self.declare_gen(ch=cfg['gen_ch'], nqz=1) # nqz=1 means no frequency folding
self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'])
# Define a constant pulse (no envelope shaping)
self.add_pulse(
ch=cfg['gen_ch'],
name="my_pulse",
style="const", # Constant amplitude
freq=cfg['freq'], # Frequency in MHz
ro_ch=cfg['ro_ch'], # Round freq to the readout's grid too (frequency matching)
length=cfg['pulse_len'], # Duration in microseconds
phase=cfg['phase'], # Phase in degrees
gain=cfg['gain'] # Amplitude (0 to 1)
)
# Configure the readout
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):
# Play the pulse at time t=0
self.pulse(ch=cfg['gen_ch'], name="my_pulse", t=0)
# Trigger the readout
self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'])
# Configure the experiment
config = {
'gen_ch': GEN_CH,
'ro_ch': RO_CH,
'freq': 500.0, # MHz
'pulse_len': 0.1, # microseconds
'phase': 0, # degrees
'gain': 1.0, # full amplitude
'trig_time': 0.2, # microseconds (when to start readout)
'ro_len': 0.5 # microseconds (readout duration)
}
# Create and run the program
prog = SinglePulseProgram(soccfg, reps=1, final_delay=0.5, cfg=config)
# Acquire data (decimated mode gives IQ traces vs time)
iq_data = prog.acquire_decimated(soc, rounds=10) # 10 averages
time_axis = prog.get_time_axis(ro_index=0)
# Plot the results
plt.figure(figsize=(10, 5))
plt.plot(time_axis, iq_data[0][:,0], label='I (In-phase)')
plt.plot(time_axis, iq_data[0][:,1], label='Q (Quadrature)')
plt.plot(time_axis, np.abs(iq_data[0].dot([1,1j])), label='Magnitude')
plt.legend()
plt.xlabel('Time (µs)')
plt.ylabel('Amplitude (a.u.)')
plt.title('Single Pulse Measurement')
plt.grid(True)
plt.show()
Explanation of the cell:
declare_gen()anddeclare_readout()tell the tProc which hardware resources we’ll useadd_pulse()defines a pulse with its parameters. A constant pulse (style="const") has flat amplitude.ro_chinadd_pulse()andgen_chinadd_readoutconfig()frequency-match the generator and the readout, so both use exactly the same frequency. Without them the phase of the measured I/Q slides over time and changes from run to run; see How to ensure frequency matching.In
_body(),pulse()schedules the pulse, andtrigger()starts the readout acquisitionacquire_decimated()returns averaged IQ data. Theroundsparameter controls how many times the entire experiment is repeated for averaging.
4. Multiple Pulses in One Sequence
You can define many pulses in _initialize() and play them in any order.
[8]:
class MultiPulseProgram(AveragerProgramV2):
def _initialize(self, cfg):
self.declare_gen(ch=cfg['gen_ch'], nqz=1)
self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'])
# Define a Gaussian envelope
ramp_len = 0.2
self.add_gauss(
ch=cfg['gen_ch'],
name="gauss_env",
sigma=ramp_len/10,
length=ramp_len,
even_length=True
)
# Define different pulse types
# Flat-top pulse (Gaussian rise/fall, constant middle)
self.add_pulse(
ch=cfg['gen_ch'], name="p_flattop",
style="flat_top", envelope="gauss_env",
freq=cfg['freq'], ro_ch=cfg['ro_ch'], length=0.1, phase=0, gain=1.0
)
# Gaussian pulse
self.add_pulse(
ch=cfg['gen_ch'], name="p_gauss",
style="arb", envelope="gauss_env",
freq=cfg['freq'], ro_ch=cfg['ro_ch'], phase=0, gain=1.0
)
# Constant pulse
self.add_pulse(
ch=cfg['gen_ch'], name="p_const",
style="const",
freq=cfg['freq'], ro_ch=cfg['ro_ch'], length=0.15, phase=0, gain=1.0
)
# Flat-top pulse with 90-degree phase
self.add_pulse(
ch=cfg['gen_ch'], name="p_flattop_90",
style="flat_top", envelope="gauss_env",
freq=cfg['freq'], ro_ch=cfg['ro_ch'], length=0.1, phase=90, 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):
self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'], ddr4=True)
# Sequence of pulses at different times (microseconds)
self.pulse(ch=cfg['gen_ch'], name="p_flattop", t=0)
self.pulse(ch=cfg['gen_ch'], name="p_gauss", t=0.4)
self.pulse(ch=cfg['gen_ch'], name="p_const", t=0.8)
self.pulse(ch=cfg['gen_ch'], name="p_flattop_90", t=1.2)
self.pulse(ch=cfg['gen_ch'], name="p_gauss", t=1.6)
# Run the multi-pulse sequence
config['ro_len'] = 2.0 # Longer readout to capture all pulses
prog = MultiPulseProgram(soccfg, reps=1, final_delay=0.5, cfg=config)
iq_data = prog.acquire_decimated(soc, rounds=10)
time_axis = prog.get_time_axis(ro_index=0)
plt.figure(figsize=(12, 6))
plt.plot(time_axis, iq_data[0][:,0], label='I')
plt.plot(time_axis, iq_data[0][:,1], label='Q')
plt.plot(time_axis, np.abs(iq_data[0].dot([1,1j])), label='Magnitude')
plt.legend()
plt.xlabel('Time (µs)')
plt.ylabel('Amplitude (a.u.)')
plt.title('Multi-Pulse Sequence')
plt.grid(True)
plt.show()
Key observations from this example:
Once a pulse is defined, it can be reused multiple times (
p_gaussappears twice)Different pulse styles serve different purposes:
constfor square pulses,arbfor shaped pulses,flat_topfor shaped pulses with a constant topThe
even_length=Trueparameter ensures the envelope has an even number of samples, which is required for some operations
5. Inspecting Your Program
You can print the program to see what the tProc will execute:
[9]:
print(prog)
macros:
WriteReg(dst='s_core_w1', src=0)
ConfigReadout(ch=0, name='my_ro', t=0, tag=None, t_params={'t': <qick.asm_v2.QickParam object at 0xffff6e06a500>}, t_regs={'t': 0})
Delay(t=1.0, auto=True, gens=True, ros=True, tag=None, t_params={'t': <qick.asm_v2.QickParam object at 0xffff6e06a590>}, t_regs={'t': 430})
OpenLoop(n=1, name='reps')
Trigger(ros=[0], tts=[], pins=[0], t=0.2, width=0.023251488095238096, ddr4=True, mr=False, tag=None, t_params={'t': <qick.asm_v2.QickParam object at 0xffff6e06a770>, 'width': <qick.asm_v2.QickParam object at 0xffff6e06a560>}, t_regs={'t': 86, 'width': 10}, outdict=defaultdict(<class 'int'>, {}), trigset={0, 9, 10})
Pulse(ch=0, name='p_flattop', t=0, tag=None, t_params={'t': <qick.asm_v2.QickParam object at 0xffff6e06a650>}, t_regs={'t': 0})
Pulse(ch=0, name='p_gauss', t=0.4, tag=None, t_params={'t': <qick.asm_v2.QickParam object at 0xffff6e06a800>}, t_regs={'t': 172})
Pulse(ch=0, name='p_const', t=0.8, tag=None, t_params={'t': <qick.asm_v2.QickParam object at 0xffff6e06a6e0>}, t_regs={'t': 344})
Pulse(ch=0, name='p_flattop_90', t=1.2, tag=None, t_params={'t': <qick.asm_v2.QickParam object at 0xffff6e06a7d0>}, t_regs={'t': 516})
Pulse(ch=0, name='p_gauss', t=1.6, tag=None, t_params={'t': <qick.asm_v2.QickParam object at 0xffff6e06a8c0>}, t_regs={'t': 688})
Wait(t=0, auto=True, gens=False, ros=True, tag=None, no_warn=True, t_params={'t': <qick.asm_v2.QickParam object at 0xffff6e06a920>}, t_regs={'t': 946})
Delay(t=0.5, auto=True, gens=True, ros=True, tag=None, t_params={'t': <qick.asm_v2.QickParam object at 0xffff6e06a9e0>}, t_regs={'t': 1161})
IncReg(dst='s_core_w1', src=1)
CloseLoop()
End()
registers:
reps: QickRegisterV2(addr=0, init=None)
pulses:
p_flattop: QickPulse(waveforms=[Waveform(name='p_flattop_w0', freq=QickRawParam(par='freq', start=224054700, spans={}, quantize=10, steps={}), phase=QickRawParam(par='phase', start=0, spans={}, quantize=1, steps={}), env=0, gain=QickRawParam(par='gain', start=32766, spans={}, quantize=2, steps={}), length=60, conf=8), Waveform(name='p_flattop_w1', freq=QickRawParam(par='freq', start=224054700, spans={}, quantize=10, steps={}), phase=QickRawParam(par='phase', start=0, spans={}, quantize=1, steps={}), env=0, gain=QickRawParam(par='gain', start=16383, spans={}, quantize=1, steps={}), length=QickRawParam(par='length', start=60, spans={}, quantize=1, steps={}), conf=9), Waveform(name='p_flattop_w2', freq=QickRawParam(par='freq', start=224054700, spans={}, quantize=10, steps={}), phase=QickRawParam(par='phase', start=0, spans={}, quantize=1, steps={}), env=60, gain=QickRawParam(par='gain', start=32766, spans={}, quantize=2, steps={}), length=60, conf=8)])
p_gauss: QickPulse(waveforms=[Waveform(name='p_gauss_w0', freq=QickRawParam(par='freq', start=224054700, spans={}, quantize=10, steps={}), phase=QickRawParam(par='phase', start=0, spans={}, quantize=1, steps={}), env=0, gain=QickRawParam(par='gain', start=32766, spans={}, quantize=1, steps={}), length=120, conf=8)])
p_const: QickPulse(waveforms=[Waveform(name='p_const_w0', freq=QickRawParam(par='freq', start=224054700, spans={}, quantize=10, steps={}), phase=QickRawParam(par='phase', start=0, spans={}, quantize=1, steps={}), env=0, gain=QickRawParam(par='gain', start=32766, spans={}, quantize=1, steps={}), length=QickRawParam(par='length', start=90, spans={}, quantize=1, steps={}), conf=9)])
p_flattop_90: QickPulse(waveforms=[Waveform(name='p_flattop_90_w0', freq=QickRawParam(par='freq', start=224054700, spans={}, quantize=10, steps={}), phase=QickRawParam(par='phase', start=1073741824, spans={}, quantize=1, steps={}), env=0, gain=QickRawParam(par='gain', start=32766, spans={}, quantize=2, steps={}), length=60, conf=8), Waveform(name='p_flattop_90_w1', freq=QickRawParam(par='freq', start=224054700, spans={}, quantize=10, steps={}), phase=QickRawParam(par='phase', start=1073741824, spans={}, quantize=1, steps={}), env=0, gain=QickRawParam(par='gain', start=16383, spans={}, quantize=1, steps={}), length=QickRawParam(par='length', start=60, spans={}, quantize=1, steps={}), conf=9), Waveform(name='p_flattop_90_w2', freq=QickRawParam(par='freq', start=224054700, spans={}, quantize=10, steps={}), phase=QickRawParam(par='phase', start=1073741824, spans={}, quantize=1, steps={}), env=60, gain=QickRawParam(par='gain', start=32766, spans={}, quantize=2, steps={}), length=60, conf=8)])
my_ro: QickPulse(waveforms=[Waveform(name='my_ro_w0', freq=QickRawParam(par='freq', start=-873813330, spans={}, quantize=-39, steps={}), phase=0, env=0, gain=0, length=3, conf=264)])
expanded ASM:
NOP
REG_WR s12 imm #0
WPORT_WR p4 wmem [&8] @0
TIME #430 inc_ref
REG_WR r0 imm #0
reps:
TRIG p0 set @86
TRIG p9 set @86
TRIG p10 set @86
TRIG p0 clr @96
TRIG p9 clr @96
TRIG p10 clr @96
WPORT_WR p0 wmem [&0] @0
WPORT_WR p0 wmem [&1] @0
WPORT_WR p0 wmem [&2] @0
WPORT_WR p0 wmem [&3] @172
WPORT_WR p0 wmem [&4] @344
WPORT_WR p0 wmem [&5] @516
WPORT_WR p0 wmem [&6] @516
WPORT_WR p0 wmem [&7] @516
WPORT_WR p0 wmem [&3] @688
REG_WR s15 label SKIP
WAIT [s15] @946 time
TIME #1161 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]
Explanation: This prints the compiled program in three sections:
macros: High-level operations (pulse definitions, loops, triggers)
registers: Allocated data registers
pulses: Defined waveforms with their parameters
expanded ASM: The actual assembly code that runs on the tProc
Understanding this output becomes important when debugging complex sequences.
6. Summary
You have learned:
The structure of a tProc v2 program (
_initializevs_body)How to define and play pulses of different styles (const, arb, flat_top)
How to trigger readout and acquire IQ data
How to inspect the generated assembly code
Next steps: Proceed to `02_Parameter_Sweeps.ipynb <./02_Parameter_Sweeps.ipynb>`__ to learn how to automate parameter variation and create 2D scans.