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
To run Spectrum hardware-control examples, install the optional extra:
uv sync --dev --extra control-hardware
import numpy as np
from awgsegmentfactory import AWGProgramBuilder
fs = 625e6
ir = (
AWGProgramBuilder()
.logical_channel("H")
.logical_channel("V")
# Uncalibrated channels interpret amps as RF amplitudes in mV.
.define("init_H", logical_channel="H", freqs=[90e6], amps=[300.0], phases="auto")
.define("init_V", logical_channel="V", freqs=[100e6], amps=[300.0], 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 (
AWGPhysicalSetupInfo,
compile_sequence_program,
quantize_resolved_ir,
)
quantized = quantize_resolved_ir(
ir,
)
physical_setup = AWGPhysicalSetupInfo(logical_to_hardware_map={"H": 0, "V": 1})
card_max_mV = 450.0 # match your AWG channel amplitude setting
compiled = compile_sequence_program(
quantized,
physical_setup=physical_setup,
full_scale_mv=card_max_mV,
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
If you want explicit control of stages, you can use:
synthesize_sequence_program(...)(float synthesis; optional GPU)quantise_and_normalise_voltage_for_awg(...)(full-scale-voltage/clip/full-scale to int16)
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).
Install the optional dependency group first: uv sync --extra control-hardware
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.
Optical calibration is attached through AWGPhysicalSetupInfo.
When a hardware channel has an AODSin2Calib, amps in the IR are interpreted as
desired optical power (arb), and synthesis converts (freq, optical_power) to RF
amplitude before waveform generation.
AODSin2Calib models a single hardware channel with:
optical_power(freq, rf_amp_mV) ≈ g(freq) * sin^2((π/2) * rf_amp_mV / v0(freq))
g(freq): maximum reachable optical power (arb) at each frequency.v0(freq): RF-amplitude scale (mV) controlling where saturation occurs (it is a function of frequency, not a frequency itself).- Inversion is used at compile time: desired optical power -> required RF amplitude.
- Behavior at limits:
- negative optical powers are clamped to
0. - requests above reachable power are clamped just below full scale of the model (
1 - y_epsin normalized space). - frequencies outside calibrated bounds are evaluated using edge-clamped normalized frequency.
- negative optical powers are clamped to
AODSin2Calib also stores:
freq_min_hzandfreq_max_hz: where data supports the fit.traceability_string: free-form provenance (filename, lab-book note, date, etc).- derived
best_freq_hz: frequency with highest fittedg(freq)within[freq_min_hz, freq_max_hz].
Built-in calibration objects (src/awgsegmentfactory/calibration.py):
-
AODSin2Calib- single-channel model + metadata.
- serializable via
serialise/deserialise.
-
AWGPhysicalSetupInfo- container passed to
compile_sequence_program(..., physical_setup=...). - serializable via
serialise/deserialiseandto_file/from_file. - fields:
logical_to_hardware_map: Dict[str, int]channel_calibrations: Tuple[Optional[AODSin2Calib], ...]- derived
N_chproperty
- routes each logical channel in the IR to the correct physical-channel calibration.
- if a channel calibration is
None, that channel uses raw IR amplitudes as RF amplitudes. By convention these are RF amplitudes in mV. - if a channel has
AODSin2Calib, that channel uses IR amplitudes as optical power (arb) and converts to RF amplitudes (mV) during synthesis.
- container passed to
full_scale_mvincompile_sequence_program(...)andquantise_and_normalise_voltage_for_awg(...)is the AWG output voltage (mV) that maps tofull_scale.clipdefaults to1.0in both APIs.- If your card is configured to
card_max_mV, use:full_scale_mv = card_max_mV
- This convention works for both:
- uncalibrated channels (IR amplitudes already represent RF mV),
- calibrated channels (
AODSin2Caliboutputs RF mV from optical power requests).
- Measure calibration data from your setup:
- DE-compensation JSON (
DE_RF_calibration), .awgde,- or CSV point cloud
(freq_MHz, rf_amp_mV, power_arb).
- DE-compensation JSON (
- Fit calibrations with:
python -m awgsegmentfactory.tools.fit_optical_power_calibration ...- pass one
--input-data-fileper hardware channel. - optionally set mapping with repeated
--logical-to-hardware-map, e.g.H=0,V=1. - optionally set provenance with repeated
--traceability-string.
- Save JSON output (
--write-out) and load asAWGPhysicalSetupInfo.from_file(...). - Compile with calibration:
compile_sequence_program(..., physical_setup=awg_physical_setup).
Examples:
examples/fit_optical_power_calibration.py(fit and inspect generatedAODSin2Calib/AWGPhysicalSetupInfo)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 / AWGPhysicalSetupInfo] --> 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)returns aQuantizedIR: a quantizedResolvedIRplusSegmentQuantizationInfo.
- Samples (
src/awgsegmentfactory/synth_samples.py)synthesize_sequence_program(quantized, physical_setup=...)builds float per-segment waveforms.quantise_and_normalise_voltage_for_awg(...)applies full-scale-voltage/clip/full-scale and produces int16 buffers.compile_sequence_program(...)is a convenience wrapper for both steps.
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 duringsynthesize_sequence_program(..., physical_setup=...)/compile_sequence_program(...)to convert(freq, optical_power)→ RF synthesis amplitudes.
See TODO.md.