A small library for building AWG sequence-mode programs from a fluent, stateful builder API, then compiling them into per-segment int16 samples and a sequence step table (e.g. for Spectrum sequence replay mode).
The design goal is to keep a clear separation between:
- User intent (what you asked for)
- Compiler-friendly IR (explicit, integer-sample primitives)
- Hardware constraints (segment quantisation, wrap-continuous holds, minimum sizes)
- Samples (what you upload to the card)
This package uses uv to manage dependencies. To run any of the examples:
uv run examples/recreate_current.py
Python requirement: >=3.13 (see pyproject.toml).
If your environment blocks access to ~/.cache (e.g. some sandboxes/CI), run uv with a repo-local cache:
uv run --cache-dir .uv-cache examples/recreate_current.py
uv manages a virtualenv in .venv. Install/update deps with:
uv sync --dev
import numpy as np
from awgsegmentfactory import AWGProgramBuilder
fs = 625e6
ir = (
AWGProgramBuilder()
.logical_channel("H")
.logical_channel("V")
.define("init_H", logical_channel="H", freqs=[90e6], amps=[0.3], phases="auto")
.define("init_V", logical_channel="V", freqs=[100e6], amps=[0.3], phases="auto")
.segment("wait", mode="wait_trig") # loops until trigger
.tones("H").use_def("init_H")
.tones("V").use_def("init_V")
.hold(time=200e-6)
.segment("chirp_H", mode="once") # one-shot segment
.tones("H").move(df=+2e6, time=50e-6, idxs=[0])
.build_resolved_ir(sample_rate_hz=fs)
)
print(ir.duration_s, "seconds")from awgsegmentfactory import compile_sequence_program, quantize_resolved_ir
quantized = quantize_resolved_ir(
ir,
logical_channel_to_hardware_channel={"H": 0, "V": 1},
)
compiled = compile_sequence_program(
quantized,
gain=1.0,
clip=0.9,
full_scale=32767,
)
print("segments:", len(compiled.segments))
print("steps:", len(compiled.steps))If you have CuPy + an NVIDIA GPU available, compile_sequence_program(..., gpu=True) runs the
sample-synthesis stage on the GPU (resolve/quantize are still CPU).
output="numpy"(default): returns NumPy int16 buffers (GPU→CPU transfer once per segment).output="cupy": keeps int16 buffers on the GPU (useful for future RDMA workflows).- To convert back to NumPy, use
compiled_sequence_program_to_numpy(...).
- To convert back to NumPy, use
See examples/benchmark_pipeline.py --gpu.
Each segment can set phase_mode to control how the start phases are chosen:
manual: use the phases stored in the IR (from.define(..., phases=[...]),.add_tone(phase=...), etc.).optimise: choose start phases to reduce crest factor based on the segment's start freqs/amps.continue: continue phases across segment boundaries for tones whose frequencies match, and optimise any new/unmatched tones while keeping continued tones fixed.
Notes:
phase_modeis applied duringcompile_sequence_program(...)(sample synthesis). The debug timelineResolvedIR.to_timeline()shows pre-optimised phases..define(..., phases="auto")currently means "all zeros"; this is typically fine when usingphase_mode="optimise"/"continue".
Debug helpers live in awgsegmentfactory.debug and require the dev dependency group
(matplotlib / ipywidgets).
- Grid/timeline debug (Jupyter): see
examples/debugging.py - Sample-level debug with segment boundaries (and optional 2D spot grid): see
examples/sequence_samples_debug.py
This repo includes working Spectrum examples under examples/spcm/ (sequence mode, triggers, etc).
The library function upload_sequence_program(...) is a placeholder for a future stable API; today it raises
NotImplementedError for CPU upload and points at examples/spcm/6_awgsegmentfactory_sequence_upload.py.
If you pass an OpticalPowerToRFAmpCalib calibration object to compile_sequence_program(...), then amps in the
IR are treated as desired optical power (arbitrary units), and sample synthesis converts (freq, optical_power)
to the RF synthesis amplitudes actually used for sample generation.
Built-in calibrations (see src/awgsegmentfactory/calibration.py):
AODSin2Calib:optical_power ≈ g(freq) * sin^2((π/2) * rf_amp / v0(freq))(vendor-style saturation; invertible on the first lobe).- Small-signal limit:
sin(z)≈z, sooptical_power ≈ g * ((π/2) * rf_amp / v0)^2(square-law).
- Small-signal limit:
Typical workflow:
- Record calibration data from your setup (either per-frequency
DE vs RF amplitudecurves or iso-power(freq, amp)points). - Fit an
AODSin2Caliband save/import the constants in your experiment code. - Pass the calibration at compile time via
compile_sequence_program(..., optical_power_calib=calib).
Examples:
examples/optical_power_calibration_demo.py(toy sin² model, with/without calibration overlay)examples/fit_optical_power_calibration.py(fitAODSin2Calibfrom a calibration file and print a Python constant)examples/fit_all_calibrations_in_examples.py(fit all files inexamples/calibrations/and writeexamples/calibrations/sin2_calibration_constants.py)examples/sequence_samples_debug_sin2_calib.py(fit sin² from file, compile with calibration, and debug samples)
flowchart LR
B[AWGProgramBuilder]
I[IntentIR]
R[ResolvedIR]
Q[QuantizedIR]
C[CompiledSequenceProgram]
B -- build_intent_ir --> I
I -- resolve_intent_ir --> R
R -- quantize_resolved_ir --> Q
Q -- compile_sequence_program --> C
CAL[Calibrations: AODSin2Calib] --> C
R -- to_timeline --> TL[ResolvedTimeline]
Q -- debug --> DBG[sequence_samples_debug]
- Build (intent) (
src/awgsegmentfactory/builder.py)AWGProgramBuilderrecords your fluent calls into anIntentIR(build_intent_ir()).
- Intent IR (
src/awgsegmentfactory/intent_ir.py)IntentIRis continuous-time intent: logical channels/definitions/segments and ops withtime_sin seconds.
- Resolve (discretize) (
src/awgsegmentfactory/resolve.py+src/awgsegmentfactory/resolved_ir.py)resolve_intent_ir(intent, sample_rate_hz=...)converts seconds → integern_samplesand producesResolvedIR.
- Quantise for hardware (
src/awgsegmentfactory/quantize.py)quantize_resolved_ir(resolved, logical_channel_to_hardware_channel=...)returns aQuantizedIR: a quantizedResolvedIRplusSegmentQuantizationInfo.
- Samples (
src/awgsegmentfactory/synth_samples.py)compile_sequence_program(quantized, ...)synthesises per-segment int16 waveforms plus a sequence step table (CompiledSequenceProgram).
For plotting/state queries there is also a debug view:
ResolvedTimeline(src/awgsegmentfactory/resolved_timeline.py) andResolvedIR.to_timeline()
- Intent IR: continuous-time spec in seconds; “what you want”.
- Resolved IR: sample-quantized primitives (per-part integer sample counts); “what you mean”.
- Quantized IR: hardware-aligned segment lengths + optional wrap snapping; “what you can upload”.
- Compiled program: final int16 segment buffers + step table; “what the card plays”.
examples/compilation_stages.py– end-to-end overview of the pipeline.src/awgsegmentfactory/builder.py– fluent API and spec construction.src/awgsegmentfactory/intent_ir.py– intent IR (ops/spec types).src/awgsegmentfactory/resolve.py– resolver (IntentIR→ResolvedIR).src/awgsegmentfactory/resolved_ir.py– resolved IR dataclasses and helpers.src/awgsegmentfactory/quantize.py– quantisation (ResolvedIR→QuantizedIR) + wrap snapping.src/awgsegmentfactory/synth_samples.py– synthesis (QuantizedIR→CompiledSequenceProgram).src/awgsegmentfactory/resolved_timeline.py– debug timeline spans and interpolation.src/awgsegmentfactory/calibration.py– calibration interfaces and built-in models.src/awgsegmentfactory/optical_power_calibration_fit.py– fitting helpers forAODSin2Calib.src/awgsegmentfactory/debug/– optional plotting helpers (Jupyter + matplotlib).
phases="auto"currently means phases default to 0; use per-segmentphase_modefor crest-optimised/continued phases during compilation.OpticalPowerToRFAmpCalibcalibrations (e.g.AODSin2Calib) are consumed duringcompile_sequence_program(..., optical_power_calib=...)to convert(freq, optical_power)→ RF synthesis amplitudes.
See TODO.md.