Script workflow
Scripts and MATLINK use the same workspace service as Studio. Script workflows can save workspace paths and run IDs for reproducible reconnection; the MATLINK GUI selects experiments by name.
Run the complete synthetic example
After setup:
toolboxRoot = fileparts(fileparts(which('opendpd.setup')));
addpath(fullfile(toolboxRoot, "examples"));
summary = opendpdQuickstart();
The example creates a fresh workspace and a synthetic nonlinear PA capture,
trains small PA and DPD GRUs for two epochs on CPU, checks inference, exports a
waveform and writes matlab-dpd-output.mat. It closes its service when finished.
The MAT file includes xTest, u, uScored (the segmented form the stored report scored), info, fs and report. This is a workflow
check, not a model benchmark. Reopen it with:
p = opendpd.openProject(summary.workspace);
link = opendpd.studio(p);
Import your capture
Let x be the PA input and y its aligned output, recorded at the same sample
rate. Provide occupied signal bandwidth explicitly, in Hz, and the segment length.
p = opendpd.openProject("my-pa-workspace");
ds = opendpd.importIQ(p, x, y, Name="capture-001", ...
SampleRate=491.52e6, Bandwidth=200e6, Origin="measured", ...
SegmentSamples=2048, GuardSamples=256);
SegmentSamples has no default. It is the PSD (Welch) segment length of every spectral metric and the
interval at which evaluation restarts a model's state, so choose it for your signal; Studio's generated
signals use 512-4096.
Rows and columns are accepted. Backend storage uses float32; source precision and shape are recorded. No delay correction, normalization or gain fitting is performed at the bridge boundary. Nonfinite values and float32 overflow are rejected. Import uses the existing contiguous train/validation/test split; set the guard to cover your training frame context.
For a MAT file with numeric variables:
ds = opendpd.importMAT(p, "capture.mat", ...
InputVariable="tx", OutputVariable="rx", Name="capture-002", ...
SampleRate=491.52e6, Bandwidth=200e6, SegmentSamples=2048, Origin="measured");
MATLAB reads the file, so every MAT-file version works, v7.3 included. The MAT adapter accepts dense single/double complex vectors and real N×2 I/Q matrices. Real vectors are I-only. The source file name, SHA-256 and variable names are recorded; keep the original MAT file if its other variables matter to your experiment.
Train the PA and DPD
training = struct('epochs', 150, 'frame_length', 200, ...
'frame_stride', 100, 'seed', 0);
paJob = opendpd.trainPA(p, ds, Model="gru", Training=training);
opendpd.status(paJob)
pa = opendpd.wait(paJob, Timeout=3600);
dpdJob = opendpd.trainDPD(p, ds, PA=pa, Model="gru", Training=training);
dpd = opendpd.wait(dpdJob, Timeout=3600);
report = opendpd.result(dpd);
ModelParameters and Training use the existing OpenDPD schema, for example
struct('hidden_size', 8) and struct('epochs', 5). Device="auto" is the default: the
first detected of cuda, mps, cpu (least-squares models stay on cpu), which is also
Studio's default. Pass Device="cpu" for a small reproducible run. A complete experiment
configuration can be sent with opendpd.submit(p, config).
Training remains in Python/PyTorch and uses the model registry and validation
already used by Studio.
Apply the DPD
Use an independent xTest waveform with the training sample rate and
preprocessing:
[u, info] = opendpd.apply(dpd, xTest);
save("predistorted-input.mat", "u", "info", "xTest", "-v7");
u is a complex single column vector with the same sample count as xTest.
For a DPD run it is the predistorted PA input, before a physical or simulated
PA. Applying a PA run instead produces the modeled PA output.
apply supports gru, tres_gru, gmp, mp_ls and gmp_ls on CPU, unquantized. Each
has a test that compares its output with the Python evaluator. By default (Execution="auto") a
gru or gmp runs as a stream, with one state across the whole waveform, and the other models run
the way the run was scored; info.execution, info.execution_requested and info.execution_reason say which and why.
The default follows a measurement of the two executions after a PA (a state reset every 512 to 4096 samples costs 0.8 to
12 dB of linearisation quality for the GRU DPDs measured and at most 0.12 dB for the gmp DPDs; see
docs/performance/matlab-apply-semantics.md in the OpenDPD repository).
Execution="offline_segmented" is how the run was scored: state resets at the trained
run's frozen segment boundaries, the final segment is zero padded and the padding is trimmed
from the returned vector. tres_gru reads 16 future samples, so within 16 samples of a
segment end it sees zero padding rather than the waveform; info.limitations says so.
Calls do not share hidden state. Each call loads the recorded model in an isolated process,
so pass a whole waveform rather than repeatedly calling it for individual samples.
For a waveform that will run continuously, Execution="streaming" carries one state across
chunks (ChunkSamples) for gru and gmp, whose registered streaming variants are
gru_stream and gmp_stream. Other models are refused rather than approximated. Streaming
output is a different signal from the one the stored report scored; info.streaming records
the measured warm-up, look-ahead and chunk consistency of that execution.
Inference metadata includes checkpoint and sample hashes, execution mode, sample count, segment length, sample rate, preprocessing version and output role. Later edits to dataset metadata do not alter frozen inference settings. A checkpoint whose hash changed is refused.
To queue the standard test-split waveform export:
exported = opendpd.wait(opendpd.runDPD(dpd));
exportReport = opendpd.result(exported);
opendpd.openStudio(p, Page="run", RunID=exported.ID);
The existing runDPD exporter carries state across the whole test waveform, as apply does by default for a
gru or gmp; Execution="offline_segmented" resets at segment boundaries, as the stored report was scored. These
outputs can differ. Read the export
sidecar for its execution semantics. The quickstart saves the segmented
training-run report alongside apply's waveform and separately records the
export run ID. Stored DPD reports evaluate a PA surrogate. A new waveform passed
to apply does not update that stored report or establish hardware performance.
One call from a capture to models: fit
When you have a paired capture and want models that run in MATLAB, fit does the whole sequence and starts Python as a
separate process (no pyenv):
[dpd, pa, report] = opendpd.fit(x, y, Workspace="my-pa-workspace", SampleRate=fs, Bandwidth=bw, ...
SegmentSamples=2048, PAModel="gru", DPDModel="gru");
u = opendpd.apply(dpd, xNew); % the predistorted PA input, plain MATLAB
yHat = opendpd.apply(pa, u); % the PA model's output for it
report.DPD.Result % the stored evaluation result of the DPD run
The runs are ordinary runs in Workspace: open it in Studio (opendpd.studio("my-pa-workspace")) to see the curves
and spectra. Use the project API (openProject, importIQ, trainPA, trainDPD) when you need finer control. See the
function reference for the options, how Python is found and what is checked first.
Score a capture without training
Metrics need no project or server. Play opendpd.waveform through your amplifier, capture its output and score it
with the code Studio uses:
w = opendpd.waveform(Seed=1, Subframes=10); % the waveform to play (loop it); w.x is 30.72 MS/s
% ... play w (resampled to your generator's rate), capture the PA output as y at SampleRate fs ...
evm = opendpd.metrics.evm(y, w, SampleRate=fs); % evm.EVM_RMS in percent
aclr = opendpd.metrics.aclr(y, SampleRate=fs, SegmentSamples=2048, Waveform=w); % aclr.ACLR_L, aclr.ACLR_R in dBc
SegmentSamples has no default because the ratio depends on it: it is the Welch segment length, and a longer
segment resolves the band edges better. Report it with the number. If Status is not "ok", read Reason; for
example "missing_reference" means the capture does not correlate with w. See the
function reference for all options.
Take a trained model out of OpenDPD
A trained PA or DPD can leave the Python environment as a package of data and run in plain MATLAB, on a machine that has no Python:
opendpd.export(dpd, "apa-dpd.opendpd.zip"); % needs the Python SDK; the same run always gives the same bytes
model = opendpd.load("apa-dpd.opendpd.zip"); % plain MATLAB from here on
report = opendpd.verify(model) % golden test: OpenDPD's outputs vs this MATLAB release, within 1e-5
u = opendpd.apply(model, xTest); % auto, like opendpd.apply(dpd, xTest): a stream for gru and gmp
y = model(chunk); reset(model); % streaming, for gru and gmp
Use the training dataset's sample rate and amplitude units: nothing is normalised or aligned
(model.Manifest.signal, model.Manifest.scaling). For an mp_ls model, model.commCoefficients() returns the matrix
for comm.DPD and rf.PAmemory, so a memory polynomial fitted by OpenDPD can be run by MathWorks code; one nperseg segment
of OpenDPD and one comm.DPD stream from a zero state agree (rf.PAmemory starts its delay line with the first sample: see the
reference for the zero pad). See the function
reference for what a package contains, what verify
shows and what it does not, and how a package from elsewhere is read.
To use the model in Simulink or with MATLAB Coder, write it as a standalone class:
r = opendpd.generateCode(model, "apa-dpd-class", Name="ApaDpd", Execution="streaming");
addpath(r.Folder); ApaDpdCheck() % the golden test again, on the generated class
Add a MATLAB System block with the System object name ApaDpd, or run codegen ApaDpdStep .... The example
opendpdSimulink builds a DPD-then-PA transmit chain. Choose streaming when samples arrive one at a time or in
frames of any size; the default restarts the state every nperseg samples, as in training. See the function
reference.
A fixed-point deployment package (opendpd deploy, or the Studio's Deployment panel) is loaded the same way and checked bit
for bit against its golden vectors; the same model gives you the expected integers for your own implementation:
fixed = opendpd.load("deploy.zip"); % an opendpd.FixedModel: integers held exactly in double precision
opendpd.verify(fixed).status % "bit_exact", or the case, sample and signal where it first differs
[yq, state, trace] = fixed.runInteger(xq); % integers in, integers out, the state after every sample
See the function reference.
Measure on your bench
opendpd.lab.Session wraps the instrument code you already have so that a measurement is supervised and recorded
(see the function reference for the rules). Learn the procedure on the
dry-run mock first; it emits nothing and needs no Python:
rng(1); u = 0.1 * complex(randn(8192, 1), randn(8192, 1)); % any complex baseband signal below MaxPeak
lab = opendpd.lab.Session(Instrument=opendpd.lab.MockInstrument(), Operator="Your Name");
arm(lab);
y = measure(lab, u, SampleRate=30.72e6); % the mock's synthetic PA, not an amplifier
disarm(lab);
record(lab) % operator, limits, hashes of played and captured, log
For your own chain, replace Instrument= by MeasureFcn= and RFOffFcn= (and, if you have them, HeartbeatFcn= and
PowerCalibration=). Arming such a session needs the person's name and the environment variable
OPENDPD_ALLOW_RF_OUTPUT=1; nothing in this toolbox sets that variable.
Reconnect, cancel and stop
MATLINK report delivery does not require script IDs. Use its Send to MATLAB or Send when ready actions, then select Open in MATLAB to inspect the saved report. For scripts, keep Job handles during a session and save IDs for later reconnection:
savedRunID = dpd.ID;
savedWorkspace = p.Workspace;
opendpd.disconnect(p.Workspace); % if MATLINK is attached
opendpd.closeProject(p); % disconnect; service and jobs continue
p = opendpd.openProject(savedWorkspace);
job = opendpd.getRun(p, savedRunID);
opendpd.status(job)
opendpd.cancel(job); % explicit cancellation request, when applicable
If MATLINK is attached, opendpd.disconnect(p.Workspace) detaches that bridge
without stopping training or closing a caller-owned Project. Calling
opendpd.disconnect() detaches all MATLINK bridges in this MATLAB process.
Disconnecting ends pending report delivery; reconnect and request a saved report
again when needed.
Save paths and run IDs as strings; MATLAB handles containing Python objects
are not portable model artifacts. A timeout or Ctrl+C during wait leaves the
job running. Cancellation remains cancel_requested until the worker stops.
opendpd.closeProject(p, StopService=true) stops an SDK-started service and all
jobs in that workspace, including after reconnecting. Stop externally launched
Studio through its original launcher. Uninstalling the toolbox leaves Python
environments and experiment workspaces intact.