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:

  1. Zurich Instruments LabOne Q - OutputSimulator compatible format

  2. Xilinx RFSoC / QICK - DAC-ready int16 waveforms

  3. ARTIQ / Sinara - DDS/URUKUL compatible format

  4. 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)

export_to_laboneq()

RFSoC/QICK

NPZ (int16 I/Q)

export_to_rfsoc_qick()

ARTIQ/Sinara

HDF5 (float32)

export_to_artiq()

Generic AWG

CSV / HDF5

export_to_csv() / export_to_hdf5()

Calibration

JSON (parametric)

export_pulse_templates()

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.