Tutorial: Hardware Export - LabOne Q, RFSoC/QICK, and ARTIQ
This tutorial shows how to export waveforms from qiskit-waveform-sim to various hardware control systems:
Zurich Instruments LabOne Q -
OutputSimulatorcompatible formatXilinx RFSoC / QICK - DAC-ready int16 waveforms
ARTIQ / Sinara - DDS/URUKUL compatible format
Generic AWG - CSV/HDF5 for arbitrary waveform generators
[1]:
import json
import numpy as np
from qiskit import QuantumCircuit
from qiskit.providers.fake_provider import GenericBackendV2
from qiskit.transpiler import generate_preset_pass_manager
from qiskit_waveform_sim import TargetWaveformSimulator
Setup: Create a Test Circuit
[2]:
backend = GenericBackendV2(num_qubits=2, seed=42)
qc = QuantumCircuit(2, 2)
qc.h(0)
qc.cx(0, 1)
qc.rx(np.pi/4, 0)
qc.rzz(np.pi/2, 0, 1)
qc.measure([0, 1], [0, 1])
pm = generate_preset_pass_manager(
optimization_level=1, backend=backend, scheduling_method="alap"
)
scheduled_qc = pm.run(qc)
sim = TargetWaveformSimulator(backend.target).compile(scheduled_qc)
total_dt = sim.total_duration_dt
dt_sec = sim.dt
print(f"Circuit duration: {total_dt} dt = {total_dt * dt_sec * 1e6:.2f} µs")
print(f"Sample rate: {1/dt_sec/1e9:.2f} GSa/s")
Circuit duration: 16752 dt = 3.72 µs
Sample rate: 4.50 GSa/s
1. Extract Full Waveforms
[3]:
# Extract all channels for full circuit duration
waveforms = {}
for ch in sim.get_channels():
snip = sim.get_snippet(ch, start_dt=0, length_dt=total_dt)
waveforms[ch] = {
"time_ns": snip.time_ns,
"I": snip.wave.real,
"Q": snip.wave.imag,
"phase_rad": snip.phase_rad,
"dt_sec": snip.dt_sec,
"events": [
{
"op_name": ev.op_name,
"start_dt": ev.start_dt,
"duration_dt": ev.duration_dt,
"shape": ev.shape,
"amp": complex(ev.amp),
"beta": ev.beta,
"sigma_dt": ev.sigma_dt,
"risefall_dt": ev.risefall_dt,
"frame_phase_rad": ev.frame_phase_rad,
}
for ev in snip.events
],
}
print(f"{ch}: {len(snip.wave)} samples, {len(snip.events)} events")
d0: 16752 samples, 8 events
d1: 16752 samples, 1 events
m0: 16752 samples, 1 events
m1: 16752 samples, 1 events
u(0,1): 16752 samples, 6 events
2. Export to LabOne Q Format
[4]:
def export_to_laboneq(waveforms, dt_sec, filename="waveforms_laboneq.json"):
"""
Export waveforms in a format compatible with LabOne Q's waveform
upload. LabOne Q expects waveforms as complex arrays with
associated sampling rate.
"""
export_data = {
"sampling_rate_hz": 1.0 / dt_sec,
"waveforms": {},
}
for ch, data in waveforms.items():
# LabOne Q uses complex64 for waveform data
wave_complex = data["I"] + 1j * data["Q"]
export_data["waveforms"][ch] = {
"samples": [[float(c.real), float(c.imag)] for c in wave_complex.astype(np.complex64).tolist()],
"length": len(wave_complex),
"markers": np.zeros(len(wave_complex), dtype=np.uint8).tolist(),
}
with open(filename, 'w') as f:
json.dump(export_data, f, indent=2)
print(f"Exported LabOne Q format to {filename}")
return export_data
# Note: For actual LabOne Q integration, use their Python API:
# from zhinst.toolkit import Session
# session = Session("localhost") # or device IP
# device = session.connect_device("DEV1234")
# awg = device.awgs[0]
# awg.upload_waveform("wave_d0", waveform_data)
laboneq_data = export_to_laboneq(waveforms, dt_sec)
Exported LabOne Q format to waveforms_laboneq.json
3. Export to RFSoC / QICK Format
[5]:
def export_to_rfsoc_qick(waveforms, dt_sec, filename="waveforms_rfsoc.npz"):
"""
Export waveforms for Xilinx RFSoC running QICK firmware.
QICK expects int16 DAC values (signed 16-bit) for I and Q.
"""
# RFSoC DAC typically 14-bit effective, scale to int16 range
DAC_MAX = 32767 # 2^15 - 1
export_data = {
"sampling_rate_hz": 1.0 / dt_sec,
"channels": {},
}
for ch, data in waveforms.items():
# Scale to DAC range (assuming input normalized to [-1, 1])
I_dac = np.clip(np.round(data["I"] * DAC_MAX), -DAC_MAX, DAC_MAX).astype(np.int16)
Q_dac = np.clip(np.round(data["Q"] * DAC_MAX), -DAC_MAX, DAC_MAX).astype(np.int16)
export_data["channels"][ch] = {
"I": I_dac,
"Q": Q_dac,
"length": len(I_dac),
}
np.savez_compressed(filename, **export_data)
print(f"Exported RFSoC/QICK format to {filename}")
return export_data
# For actual QICK usage:
# from qick import QickConfig
# soc = QickConfig("config.json")
# soc.load_waveforms(waveforms_dict)
rfsoc_data = export_to_rfsoc_qick(waveforms, dt_sec)
Exported RFSoC/QICK format to waveforms_rfsoc.npz
4. Export to ARTIQ / Sinara Format
[6]:
def export_to_artiq(waveforms, dt_sec, filename="waveforms_artiq.h5"):
"""
Export waveforms for ARTIQ/Sinara DDS/URUKUL systems.
Uses HDF5 format with datasets for each channel.
Requires h5py: pip install h5py
"""
try:
import h5py
except ImportError:
print("h5py not installed, skipping ARTIQ export")
return None
with h5py.File(filename, 'w') as f:
f.attrs['sampling_rate_hz'] = 1.0 / dt_sec
f.attrs['dt_sec'] = dt_sec
for ch, data in waveforms.items():
grp = f.create_group(ch)
grp.create_dataset('I', data=data["I"].astype(np.float32))
grp.create_dataset('Q', data=data["Q"].astype(np.float32))
grp.create_dataset('phase_rad', data=data["phase_rad"].astype(np.float32))
grp.create_dataset('time_ns', data=data["time_ns"].astype(np.float64))
# Store event metadata as attributes
for i, ev in enumerate(data["events"]):
ev_grp = grp.create_group(f"event_{i}")
for k, v in ev.items():
if isinstance(v, complex):
ev_grp.attrs[f"{k}_real"] = v.real
ev_grp.attrs[f"{k}_imag"] = v.imag
else:
ev_grp.attrs[k] = v
print(f"Exported ARTIQ format to {filename}")
return filename
# artiq_file = export_to_artiq(waveforms, dt_sec)
5. Export to Generic CSV/HDF5 for AWGs
[7]:
def export_to_csv(waveforms, dt_sec, prefix="waveform"):
"""Export each channel to CSV for generic AWG import."""
for ch, data in waveforms.items():
filename = f"{prefix}_{ch}.csv"
# Stack time, I, Q, phase
csv_data = np.column_stack([
data["time_ns"],
data["I"],
data["Q"],
data["phase_rad"],
])
header = "time_ns,I,Q,phase_rad"
np.savetxt(filename, csv_data, delimiter=",", header=header, comments='')
print(f"Exported {ch} to {filename} ({len(csv_data)} rows)")
export_to_csv(waveforms, dt_sec)
Exported d0 to waveform_d0.csv (16752 rows)
Exported d1 to waveform_d1.csv (16752 rows)
Exported m0 to waveform_m0.csv (16752 rows)
Exported m1 to waveform_m1.csv (16752 rows)
Exported u(0,1) to waveform_u(0,1).csv (16752 rows)
[8]:
def export_to_hdf5(waveforms, dt_sec, filename="waveforms.h5"):
"""Export all channels to single HDF5 file."""
try:
import h5py
except ImportError:
print("h5py not installed, skipping HDF5 export")
return
with h5py.File(filename, 'w') as f:
f.attrs['sampling_rate_hz'] = 1.0 / dt_sec
f.attrs['dt_sec'] = dt_sec
f.attrs['total_duration_ns'] = total_dt * dt_sec * 1e9
for ch, data in waveforms.items():
grp = f.create_group(ch)
grp.create_dataset('I', data=data["I"].astype(np.float32), compression='gzip')
grp.create_dataset('Q', data=data["Q"].astype(np.float32), compression='gzip')
grp.create_dataset('phase_rad', data=data["phase_rad"].astype(np.float32), compression='gzip')
grp.create_dataset('time_ns', data=data["time_ns"].astype(np.float64), compression='gzip')
print(f"Exported HDF5 to {filename}")
export_to_hdf5(waveforms, dt_sec)
Exported HDF5 to waveforms.h5
6. Parameterized Pulse Templates for Calibration
[9]:
# For calibration workflows, export parametric pulse templates
# that can be scaled at runtime
def export_pulse_templates(sim, filename="pulse_templates.json"):
"""
Export pulse templates with parametric formulas instead of
pre-computed samples. Hardware can evaluate these at runtime.
"""
templates = {}
for ch in sim.get_channels():
events = sim.events_by_channel.get(ch, [])
channel_templates = []
for ev in events:
if ev.duration_dt == 0:
continue
template = {
"name": ev.op_name,
"channel": ch,
"start_dt": ev.start_dt,
"duration_dt": ev.duration_dt,
"shape": ev.shape,
"parameters": {
"amplitude": float(abs(ev.amp)),
"phase_rad": float(np.angle(ev.amp)),
"frame_phase_rad": ev.frame_phase_rad,
},
}
if ev.shape == "drag":
template["parameters"].update({
"beta": ev.beta,
"sigma_dt": ev.sigma_dt,
})
elif ev.shape == "gaussian_square":
template["parameters"].update({
"risefall_dt": ev.risefall_dt,
})
# channel_templates.append(template)
channel_templates.append(template)
templates[ch] = channel_templates
with open(filename, 'w') as f:
json.dump(templates, f, indent=2)
print(f"Exported pulse templates to {filename}")
return templates
templates = export_pulse_templates(sim)
Exported pulse templates to pulse_templates.json
7. Verify Exported Waveforms
[10]:
# Quick verification: check exported files exist and have correct structure
import os
for f in ["waveforms_laboneq.json", "waveforms_rfsoc.npz"]:
if os.path.exists(f):
size = os.path.getsize(f)
print(f"{f}: {size/1024:.1f} KB")
# Load and verify RFSoC format
rfsoc_loaded = np.load("waveforms_rfsoc.npz", allow_pickle=True)
print(f"\nRFSoC loaded keys: {list(rfsoc_loaded.keys())}")
for k in rfsoc_loaded.keys():
if k != "sampling_rate_hz":
print(f" {k}: {type(rfsoc_loaded[k])}")
waveforms_laboneq.json: 5861.7 KB
waveforms_rfsoc.npz: 4.3 KB
RFSoC loaded keys: ['sampling_rate_hz', 'channels']
channels: <class 'numpy.ndarray'>
Summary
Target Platform |
Format |
Key Function |
|---|---|---|
LabOne Q |
JSON (complex64) |
|
RFSoC/QICK |
NPZ (int16 I/Q) |
|
ARTIQ/Sinara |
HDF5 (float32) |
|
Generic AWG |
CSV / HDF5 |
|
Calibration |
JSON (parametric) |
|
All export functions are provided as examples - adapt them to your specific hardware API requirements. The core TargetWaveformSimulator provides the exact I/Q samples with correct frame tracking that any hardware platform needs.