Skip to content

OpenDPD Studio 2.4.0

OpenDPD 2.4.0 adds the MATLAB toolbox, MATLINK, the Python SDK and two independently checked LTE EVM/ACLR profiles. Python, Studio and the MATLAB toolbox share the 2.4.0 release version and SDK protocol 1.

uv pip install --python .venv --upgrade "opendpd==2.4.0" --torch-backend=auto
uv run --no-project --python .venv opendpd gui

The wheel includes Studio. Download OpenDPD-2.4.0.mltbx from the GitHub release and install it in MATLAB; it is not published to Add-On Explorer or File Exchange. See the MATLAB guide.

The detailed development checks below retain their original counts and scope. Historical mutation counts came from a throwaway harness, not a committed, reproducible test suite; the commit messages and this note are their record. The Not verified section states the remaining platform and hardware limits.

For MATLAB users

The toolbox in Matlab/toolbox (source-only .mltbx, built locally) takes a MATLAB engineer from a paired capture to a trained PA and DPD, and from a trained model to Simulink and generated C, without leaving MATLAB:

  • Studio from MATLAB (MATLINK). opendpd.studio, importIQ, trainPA, trainDPD, getRun, apply and the rest of the script API drive a local Studio service. The PSD segment length is always stated by the caller; there is no default.
  • One call from capture to models. opendpd.fit(x, y, SampleRate=..., Bandwidth=..., SegmentSamples=...) trains a PA and then a DPD against it and returns two opendpd.Model objects. Python runs as a separate process started with java.lang.ProcessBuilder, so MATLAB never loads pyenv; a service that fit started itself is stopped again, one that was already running is left alone.
  • Models that leave Python as data. opendpd.export (or opendpd export-model RUN_ID --workspace WS --out FILE from a terminal) writes an opendpd-model-v1 package: weights, a statement of the two execution semantics, signal metadata, and a golden test vector made of OpenDPD's own outputs; no code, deterministic bytes. opendpd.load, opendpd.verify and opendpd.apply(model, x) run it in plain MATLAB with no Python, project, server or other toolbox. Models: gru, tres_gru, gmp, mp_ls, gmp_ls. Format specification: model package.
  • Standalone classes for Simulink and MATLAB Coder. opendpd.generateCode(model, folder) writes a matlab.System with the weights as constants, a Coder entry point, a golden check and a README. The class needs nothing from the toolbox. examples/opendpdSimulink.m builds a DPD-then-PA chain.
  • Fixed-point deployment packages. opendpd.load also reads a fixed-point-v1 package (written by opendpd deploy or the Studio's Deployment panel) as an opendpd.FixedModel: opendpd.verify replays its six golden vectors and compares every output sample and every state step bit for bit, model.runInteger(xq) takes integers in and gives integers and the state trace out, and opendpd.apply(model, x) runs a waveform through the integer model. The integers are held exactly in double precision, so it needs no Fixed-Point Designer, compiler or Python; it is a second implementation to check against the C99 reference and a source of expected values for your own (HLS, RTL, firmware).
  • Memory-polynomial models in MathWorks objects. For mp_ls, model.commCoefficients() is the Coefficients of comm.DPD and the CoefficientMatrix of rf.PAmemory; rf.PAmemory starts its delay line with the first input sample, so a zero history needs a pad of Q - 1 zeros (documented, and in info.rf_pamemory).
  • Metrics without a run. opendpd.metrics.evm, aclr and evaluate score a capture with Studio's procedures, with no project or server.
  • Supervised measurement. opendpd.lab.Session wraps instrument code you already have with an RF-off interlock and a record that carries the SHA-256 of every played and captured signal into the dataset. Exercised only on a dry-run mock.

Evidence

Agreement with MathWorks tools

Both comparisons were registered before any number existed (rules, signals, budgets, scored/diagnostic split) and are reported with everything that disagrees.

  • Memory polynomial against comm.DPDCoefficientEstimator and comm.DPD: passes as registered. 21 of 21 scored items are within budget, the largest at 2.5·10⁻⁸ against a budget of 10⁻⁶ (the float32 interface). Coefficient vectors agree to 3.9·10⁻¹³ on the complete-memory rows; comm.DPD runs OpenDPD's coefficients to 2.5·10⁻⁸ relative. Two practical findings: the gain setting must be DesiredAmplitudeGaindB = 10·log10(G) (reading it as 20·log10(G) gives coefficients 83 % to 97 % away), and OpenDPD's mp_ls includes the zero-filled first rows of every segment, which MathWorks' estimator does not (the coefficient vectors differ by 0.6 % to 19 % depending on degree and memory depth). Record.
  • rf.PAmemory (RF Toolbox): passes as registered (amendment 2 of the same record, registered before its script existed). The coefficient matrix has comm.DPD's layout (largest relative difference 4.2·10⁻¹⁶ over 36 cases); the delay line of rf.PAmemory starts filled with the first input sample, which is the only difference (1.3·10⁻¹⁶ over 27 cases); OpenDPD's zero-history mp_ls segment equals rf.PAmemory on a zero-padded input (4.5·10⁻⁸ against a single-precision interface, budget 10⁻⁶); and the delay line carries across calls (identical over 36 cases). The RF Blockset amplifier block, the other nonlinearity models of rf.PAmemory and other releases were not compared.
  • LTE EVM/ACLR cross-validation: does not pass as registered. 102 of 115 scored items are within budget, 7 are outside it and 6 cannot be evaluated because both tools refuse the signal. EVM agrees with an independent MathWorks-blocks chain to at most 2.1·10⁻⁹ percentage points on the ten evaluable signals, and the waveform equals lteOFDMModulate to 2.9·10⁻⁸ relative RMS. The 7 failures are ACLR values up to 0.21 dB from comm.ACPR, all explained by how the two tools integrate a band whose edge is not a bin centre (a pwelch PSD integrated with comm.ACPR's rule reproduces it to 2.5·10⁻⁴ dB). The 6 not evaluable items are the +350 Hz carrier-offset signals, which both tools refuse. The validation flag of the profile is unchanged. Record.

Versioned LTE v2 validation

The original v1 record remains 102/115 passed, 7 failed, 6 unevaluable. Its profile, frozen signals, tolerances, MATLAB comparator and result JSON are unchanged. One original OpenDPD ACLR value cannot simultaneously match two MATLAB spectral configurations whose references differ by 0.280827902 dB at a 0.1 dB tolerance each.

The separately registered v2 procedure therefore exposes two named profiles: ofdm-lte20-evm-v2 uses symmetric Hann segments, and ofdm-lte20-evm-auto-v2 uses full-record symmetric Hamming settings. Both use the specified enclosing-bin integration and acquisition that estimates coarse frequency offset before retrying the timing confidence check. Wrong references are still refused.

Five recorded local runs against real MATLAB R2026a each pass 115/115: 3 waveform, 84 ACLR and 28 EVM/frequency entries. The 42 Auto ACLR entries represent 14 distinct values repeated across three segment settings. Maximum ACLR disagreement is 7.11e-15 dB (budget 0.1 dB), and maximum EVM disagreement is 4.04e-7 percentage points (budget 0.05). The same 12 input signals and package hashes are checked before scoring. An independent Claude Code session implemented the MATLAB comparator from the written protocol; another reviewed it and independently reproduced the result.

Reports, hashes, diagnostics and limits are committed. This demonstrates agreement for the registered periodic digital signals, not 3GPP conformance, arbitrary capture acquisition or RF hardware. A separate application catalogue adds the new profiles without changing any Arena-fingerprinted source file or the 23 model entries / 233 sealed cases. The Linux MATLAB job reruns the complete comparison as a required merge check.

The model runtime

  • The six small packages in Matlab/toolbox/tests/data pass their golden test on MATLAB R2026a; the pure-MATLAB kernels reproduce OpenDPD's evaluator to at most 1.2·10⁻⁷ (the polynomial models exactly), against a tolerance of 10⁻⁵ that a package can tighten but never loosen.
  • A package is read as data: six known entry names, SHA-256 of every entry from the manifest, a hard size limit, and a strict .npy parser. The toolbox never opens the .mat files of a package, because load, whos -file and matfile call loadobj for a class on the path (shown with a trap class on R2026a). The manifest is measured before it is parsed and its text made plain, a manifest that declares fewer GRU layers than the weights hold is refused, and one more defect of this reader was found by the review described below: it could end the MATLAB process on a deeply nested manifest.

Fixed-point packages (opendpd.FixedModel)

On MATLAB R2026a, Linux:

  • Two real packages written by the code path of opendpd deploy, each with the C99 reference recorded as bit_exact, are reproduced bit for bit, outputs and the state after every sample, on all six golden vectors (5,582 samples per package). One uses the default specification; in the other every format differs (14-bit input with 12 fractional bits, 14-bit state with 13, 14-bit output with 11, 12-bit weights, a 28-bit pre-activation, a 40-bit accumulator, coarser tables), so a reader that takes a number from the default specification instead of the package fails on it. A production-size package (24 hidden units, the default 65,536-sample long vector, 72,448 golden samples) verifies bit-exact in 1.3 s.
  • The kernel equals an independent scalar int64 implementation of the specification, written after the C99 reference, on 24 random sets of formats and weights (half of them making every sum saturate); it builds with MATLAB Coder, and the MEX function reproduces the golden vectors of both packages.
  • A package is read as data: known file names only (the six golden cases the specification names and nothing else), every file hashed against the manifest, the manifest listing exactly the files that are in the archive and the hashes it gives the golden vectors being the hashes of their files, a hard size limit for each kind of file, no path from the archive used; the C sources are checked against their hashes and never compiled or run. The JSON files are measured before they are parsed and the manifest is checked member by member (see Independent review below). Formats the reader cannot compute exactly are refused (words wider than the C99 backend's 16-bit types, a pre-activation wider than 32 bits, an accumulator beyond the 53 bits double precision holds, or any sum, product or shift that could leave the range below 2^53), and so are formats the 64-bit Python and C99 references cannot represent (a shift of more than 62 places, a value of 2^63 or more). The rule texts of the specification must match the ones the toolbox implements, and a Python test compares them with opendpd/schemas/fixed_point.py; another Python test checks that the references are indeed 64-bit.
  • Seeded faults: 324 expressions in the arithmetic, the readers of both package formats, the JSON guard, the model and the entry points were each changed once and the tests run (a throwaway harness, see the note at the top). 313 are caught by a failing test, 4 by ending the MATLAB process, and 7 survive; the survivors are checks that no archive can reach, because an earlier and stricter check refuses it first (listed under Independent review below).
  • Python side: tests/unit/test_fixed_point_package.py checks that the committed packages are complete and consistent, that today's Python reference still computes exactly their golden outputs, and that today's C99 generator still writes exactly their sources (and, with a compiler, that they replay bit-exact), so a change to the specification cannot leave the oracle stale.

Independent review of the package readers

When the fixed-point reader was finished, a separate agent that had been given the code and none of my conclusions reviewed both package readers (opendpd-model-v1 and fixed-point-v1) as an attacker would, with crafted archives. It found defects that my tests had not, and one of them was older than the fixed-point work:

  • MATLAB crashed. jsondecode recurses once per level of nesting. A manifest.json nested about 40,000 arrays or 6,000 objects deep ended the MATLAB process with a segmentation fault (exit code 139), and so did the same text in spec.json or weights.json; nested objects also killed isequaln, which compared the specification with the copy in the manifest. The opendpd-model-v1 reader, written earlier on this branch, had the same defect.
  • Memory and time amplification. jsondecode holds about 360 bytes per array element, so a 64 MB file of zeros asked for 12.9 GB; a list of state resets was searched with any(resetAt == k) at every sample.
  • Formats with no reference. A format that needs a shift of 63 places or more, or a value of 2^63 or more, has no answer from the 64-bit Python and C99 references (numpy raises, C is undefined, a left shift wraps), but the reader loaded it and computed an answer of its own.
  • Gaps in validation. Values of a wrong type that MATLAB takes for the right one (a one-character string for a number, a list for a text), members the format does not define, a golden file that ends in half a value, hashes in the golden index that were never compared with the hashes of the files, and control characters (escape sequences, a bell, a carriage return) in text that is displayed.

What changed, in both readers unless stated:

  • All JSON of an archive goes through opendpd.internal.decodeJson. It measures the text first (jsonShape: a scan in chunks that skips strings and carries its state across chunk boundaries) and refuses it with opendpd:Package when it nests more than 8 levels (OpenDPD writes at most 5), holds more commas than the file can need (the fixed-point weights get a budget from the hidden size and the table limit), or has a number or word of more than 64 characters (OpenDPD writes at most 21). The last limit comes from a finding of my own while testing the first two: jsondecode takes time quadratic in the digits after the decimal point of one number (100,000 digits take 0.4 s, a million 36 s, and a 32 MB file of them would take hours). Other shapes were timed too (long strings, keys, whitespace, duplicate or colliding keys, mixed and ragged arrays): linear.
  • The fixed-point reader limits each kind of file (manifest 4 MB, specification 1 MB, weights 32 MB, and so on), the hidden units (512), the entries of a table (65,536) and the golden samples (2,097,152); the limits are in fixedLimits.m and in the reference.
  • The manifest is checked member by member: only the members opendpd/schemas/fixed_point.py defines, each of the type it defines (a logical, a one-character string or a list no longer stands in for a number or a text); the same for the specification and the weights.
  • The hashes the manifest gives the golden vectors must be the hashes of their files; a golden file must hold whole 16-bit values.
  • requireReferenceRange refuses a shift of more than 62 places and a value of 2^63 or more.
  • Text from a manifest is made plain (control characters become ?) before it is returned or shown; a message that quotes jsondecode is cut to 120 characters, because jsondecode puts the whole bad token into its message.
  • The kernel's reset list is a mask, one pass for any length.
  • A model package whose manifest declares fewer GRU layers than the weights hold is refused.

Evidence for this round (MATLAB R2026a, Linux):

  • The review's reproductions, run against the hardened toolbox inside the MATLAB session that would have crashed: 100,000-deep arrays in manifest.json, spec.json and weights.json, 8,000-deep nested objects in the manifest and in the specification, and a 300,000-element array in weights.json are all refused with opendpd:Package, and the session carries on. An entry larger than the limit for its file (32 MB for weights.json) is refused before it is extracted.
  • TestFixedPoint (275), TestPackageSecurity (42) and TestModel (48): 365 tests, all passing. Every finding above has a test that builds the hostile package from a real fixture (never stored in the repository) and checks the identifier and the message of the refusal; jsonShape is compared with a scalar reference on random text for chunk sizes from 1 byte up, and for the exact budget of the weights.
  • Seeded faults: 324 (a throwaway harness copies the toolbox, changes one expression in the copy and runs the test classes that name it: the 118 of the fixed-point work, 22 of them rewritten for the new code, plus 206 new ones for the readers' limits and type checks, the JSON guard, the model-package checks and the reset mask). 313 are caught by a failing test. 4 are caught only because they end the MATLAB process with a segmentation fault (a guard that no longer counts closing braces, that takes the last level instead of the deepest one, or that is skipped or never compared), which is the failure the guard exists to prevent. 7 survive, and each is a check that no archive can reach because a stricter one refuses first: the character set of an entry name (an exact whitelist of names follows), directory entries (their names end in /, which no whitelisted name does), two entries with names that jsondecode maps to one field (whitelisted names never do), the size of the gate-order list (the comparison with {'r';'z';'n'} that follows implies it), isreal on integer arrays (jsondecode returns no complex numbers), and the checks in the model that the golden vectors form the six cases with their four arrays (the reader hands over exactly that). I kept those checks as a second line of defence. The harness also drove the tests: its first full run on the extended list left 39 survivors, nearly all of them gaps in my tests, which are now closed.
  • decodeJson was fuzzed with 86,000 random edits of the real manifests, specifications and weights (flipped, deleted and inserted bytes, quotes, backslashes, brackets, control characters, cut and duplicated pieces): every outcome was a value or opendpd:Package, none took longer than 0.07 s. The two model manifests were mutated one field at a time in 1,854 ways (null, logical, text, list, object, negative, fractional, huge): 280 were refused with a clean error, 1,569 changed nothing the runtime reads, 5 changed the output because they changed nperseg or num_layers to another legal value (the second of these exposed the layer-count gap above), and none raised a raw MATLAB error.

Not covered by this round: the zip layer (Java's ZipFile; the toolbox only limits sizes and names), the .npz and .npy parser beyond the cases in TestPackageSecurity, packages near the limits other than synthetic ones, other MATLAB releases (a different jsondecode may have other limits), and Windows.

Generated classes (opendpd.generateCode)

On MATLAB R2026a, Linux, with MATLAB Coder (gcc) and Simulink: for each of the six packages and each execution semantics it offers (nine classes), the generated class

  • equals opendpd.apply (observed bit-identical, tested to 10⁻¹²) on waveforms of 1 to hundreds of samples and on any chunking;
  • passes its own golden check, with exactly the error opendpd.verify reports;
  • builds with MATLAB Coder as a MEX function (variable-size input for all nine; fixed-size frames, single samples and double input for the streaming gru), and the MEX function reproduces the golden vector with the same error;
  • runs in a Simulink MATLAB System block, on a frame, in Interpreted execution and in Code generation; and a streaming gru DPD followed by a streaming gru PA, one sample per time step, equals the MATLAB chain exactly in both modes.

TestCodegen has 89 tests. The MEX tests need MATLAB Coder and the Simulink tests need Simulink, and each is skipped where its product is missing. On GitHub the Linux job installs Simulink, so the Simulink tests run there; MATLAB Coder is a transformation product that MathWorks does not license on public-project CI without a batch licensing token, so the 12 tests that build MEX functions (10 in TestCodegen, 2 in TestFixedPoint) have run on the development machine only. 37 seeded defects in the generator and in the shared kernels were each caught by at least one test (the run is summarised in the commit message of the generateCode change).

What apply does by default (auto)

apply has two executions: offline_segmented (how every Studio run is scored: the state restarts every SegmentSamples samples) and streaming (one state for the whole waveform, which is what a deployed DPD does; only gru and gmp have it). The default, Execution="auto", is the second for those two models and the first for the others. The choice was measured before it was made, with the question, the material, the decision rule and the validity checks written down first (study, two dated amendments to its procedure, results appended): mean quality lost by resetting the state, in dB (q is the mean of the EVM or in-band-error gain and the worse-side ACLR gain, so a larger number is a worse DPD):

Segment length L 256 512 1024 2048 4096 the data's own nperseg
12 Arena reference GRUs (APA B, EVM and ACLR) 14.8 12.1 9.5 7.0 4.8 1.2 (L = 19,662)
3 Studio-trained GRU pairs, APA_200MHz 12.8 11.0 7.1 4.6 3.5 0 (one segment)
3 Studio-trained GRU pairs, DPA_200MHz 3.5 2.2 0.8 0.25 (L = 2,560)
3 Arena reference GMPs 0.20 0.12 0.07 0.04 0.02

Four validity checks (the Arena's published cases reproduced to 3e-4 dB; segment length equal to the record equals streaming; Job.apply equals the harness to 1.8e-7; the harness equals the evaluator on the CPU to 1e-5 dB) all pass. Not what the plan expected: it expected the effect to be below 0.5 dB from L = 2048; the table says otherwise. The Studio GPU evaluation (cuDNN TF32) differs from a CPU recomputation of the same weights by up to 0.06 dB in NMSE and IBE, which the study records but which cancels in the comparison.

Whole-suite status

The Python suite (-m "not extended", 3,295 tests) ran in eight parallel shards on the commit that precedes the generateCode work. It found one real defect (the documented command opendpd export-model was not executed by the documented-commands test; fixed) and two failures that are properties of this development machine: both fail the same way on main at 256f856 here. They are the reproduction test of test_research_review_flow (the reproduction script reports that the OpenDPD source it finds differs from the recorded one) and test_arena_cuda.py::...[apnrru-500] (a torch._inductor RecursionError while compiling the backward graph on this GPU stack). Neither touches code of this branch.

A second run, on the commit that adds the fixed-point packages and the rf.PAmemory documentation (3,306 tests, the same eight shards and the same selection), passed 3,304 and failed exactly those two.

A third run, before the apply default changed (3,307 tests, the same selection), passed 3,305 and failed exactly those two.

A fourth run on the final state of the branch (3,319 tests, the same selection, 19 extended tests deselected) passed 3,317 and failed exactly those two. All 523 MATLAB tests of the ten classes in Matlab/toolbox/tests passed (279 s, MATLAB R2026a Update 5 with MATLAB Coder and Simulink installed, so none was skipped); buildtool package built OpenDPD-2.4.0.mltbx; and the packaged toolbox, installed into a fresh MATLAB session, loaded and verified a model package and a fixed-point package, generated and checked a class, trained a small pair through the process transport (fit), connected to a Studio service and ran the quickstart, without loading Python into MATLAB until fit and MATLINK were asked to. The frontend was not touched after its last full run (326 tests, typecheck and lint); the strict documentation build has no warnings.

First runs on GitHub

.github/workflows/matlab-toolbox.yml had never run before this pull request was opened, and neither had this branch's tests on Windows or on a GitHub runner. The first runs found what the development machine could not, and the branch fixes it:

  • Three MATLAB tests did not hold on Windows: two Python one-liners had embedded double quotes, which Windows removes from a command-line argument, and a test called fileattrib(..., '+x') before the assumption that skips it there.
  • One test was not isolated from an installed OpenDPD. Its "Python without OpenDPD" found the runner's editable install, ran a complete default fit on the CPU for 16 minutes and then failed with "fit should have failed"; it passes -S now.
  • One frontend test (MatlinkGenerator) waited 1 s for a page to load, on a runner where the frontend suite takes 134 s instead of 28 s; it waits 5 s now.
  • The Python test jobs have a 30-minute limit, which cancelled two of the four on the first run and left a third at 29.5 minutes. The suite is not hung: the 247 tests added by this branch cost 8 to 10 minutes on a slow runner (test_apply_parity.py alone 280 to 440 s: about 100 Job.apply calls, each a separate Python process), and the runners of one run differ by a factor of 1.6. The limit is 45 minutes now.

With base MATLAB only, 35 of the 523 MATLAB tests skip themselves (36 on Windows, where one needs a shell). The Linux job now installs Simulink, Communications, LTE, RF, Signal Processing and DSP System Toolbox, which runs 23 of the 35 (the MATLAB System block tests in both simulation modes and the three comparisons with MathWorks tools), and it fails when a test is skipped for any other reason (OPENDPD_REQUIRE_ALL_TESTS in buildfile.m). The 12 MEX tests need MATLAB Coder, which public-project CI does not license; they remain development-machine tests (decision 8). The Windows job is marked experimental and installs base MATLAB only.

Not verified

  • MATLAB releases other than R2026a Update 5 (the package declares R2024b as a candidate minimum) and macOS. Windows has run only as GitHub's windows-2022 runner with base MATLAB (an experimental job): the tests that need a product or a shell are skipped there.
  • The 12 tests that build MEX functions with MATLAB Coder on any machine other than the development one.
  • CUDA-specific MATLAB paths, MATLAB Online, remote services, and the hosted Studio from MATLAB.
  • Any instrument: opendpd.lab.Session has run only with injected faults on the mock. No instrument adapter ships; real adapters must be written and verified by a person at the bench (docs/architecture/instruments.md). The Ctrl+C path through the session's onCleanup guard could not be exercised, because matlab -batch does not react to SIGINT.
  • Generated classes on C/C++ library or embedded targets, GPU Coder, HDL Coder, fixed-point conversion. The arithmetic is double precision on single-rounded inputs; this is not a fixed-point or HDL-ready design.
  • Fixed-point packages: other MATLAB releases and operating systems; a fi or HDL Coder implementation (none is provided); packages with more than 24 hidden units or trained longer than the fixtures' one epoch (the arithmetic does not depend on either, but only these were run); the generateCode route for them (refused with opendpd:CodegenFixedPoint); any hardware.
  • The RF Blockset amplifier block (a Simulink block that may differ from the rf.PAmemory System object), the other nonlinearity models of rf.PAmemory.
  • Packages larger than the fixtures. generateCode refuses more than 250 000 numbers and fit has been run on small synthetic captures only.
  • The LTE profile against 3GPP TS 36.104 Annex E (the deviations stay listed in the protocol), 5G NR EVM (OpenDPD has no NR profile), and anything measured.

Differences from the plan

  • The execution default of apply is auto, not offline_segmented as the plan kept it: a model with a registered streaming variant (gru, gmp) runs as a stream, every other model as the run was scored, and the metadata says which and why. The plan asked for the effect of the two executions after a PA to be measured before any default changed; it was (pre-registered in docs/performance/matlab-apply-semantics.md, six Studio-trained GRU pairs on two datasets and 15 sealed Arena checkpoints), and the decision rule fixed beforehand gave this outcome: for GRU DPDs a state reset every 512 to 4096 samples costs 0.8 to 12 dB of linearisation quality, for gmp DPDs at most 0.12 dB. The Studio test splits are one to three segments long, so the scores stored with a run do not show it; reproducing a stored score needs Execution="offline_segmented". The registry, the experimental status of the streaming variants, opendpd.generateCode and Studio itself are unchanged.
  • The CLI command is opendpd export-model, not export --format matlab.
  • fit uses a process transport (Python as a child process with files in and out), not an HTTP-native client. An HTTP transport is a design proposal only (docs/design/matlab-http-transport.md), backed by a feasibility spike that is reproducible from docs/design/matlab-http-spike/: the handshake of a real local service works from matlab.net.http alone and binary bodies stream at hundreds of MiB per second if the right MATLAB classes are used. No route was added to the server.
  • fixed-point-v1 runs in plain MATLAB as integers held in double precision (checked exact), not as fi objects, and there is no Simulink C Caller wrapper of the C99 reference. The reader refuses what double precision cannot hold exactly instead.
  • rf.PAmemory is a documented mapping with a registered check (commCoefficients() and the zero pad), not a constructor method, and the RF Blockset amplifier block is not covered.
  • The toolbox reads the .npz arrays and ignores the .mat copies (security finding above); both are in the package.
  • The model registry has no export_formats field; the supported set is EXPORT_MODELS = APPLY_MODELS in the service.
  • ARVTDNN is not an Arena reference backbone, and the PSD and inference segment lengths are not split in the data contract.
  • Feature development kept Python metadata at 2.3.1. This release aligns Python, Studio, the OpenAPI contract and toolbox at 2.4.0.

Decisions and remaining follow-ups

  1. The ofdm-lte20-evm-v1 validation flag stays pending because the original procedure did not pass. Only the two new v2 profiles are marked cross-validated.
  2. PR #80 records the original failed comparisons and the separately registered, fully passing v2 procedure. The v1 limitations remain documented.
  3. Whether to offer a complete-memory-rows option in mp_ls for coefficients that a MATLAB user can compare one to one.
  4. Whether to add export_formats to the model registry, which needs maintainer review.
  5. The coordinated release sets Python, Studio and toolbox to 2.4.0. Publication uses the reviewed main revision after all required checks pass.
  6. Whether to promote the streaming variants (gru_stream, gmp_stream) from experimental: the SDK and the MATLAB toolbox now use them by default for a waveform that will be played, on the strength of the study above, without touching the registry.
  7. Whether Studio itself (its apply and scoring paths, which stay offline_segmented) should offer or report the continuous-state execution, now that the study shows the size of the effect for waveforms longer than one or two segments.
  8. Whether to give CI a MATLAB batch licensing token with a MATLAB Coder entitlement (the secret MLM_LICENSE_TOKEN, which the workflow already passes on). The 12 MEX tests would then run on GitHub: add MATLAB_Coder to the products of the Linux job and empty its allow_skipped.

Pre-release review fixes (2026-10-11)

Independent review also bounded model-package segments and golden vectors to 2^20 samples in the Python exporter/MATLAB reader, corrected the NPZ identifier limit to 63 characters, and extended fit cancellation grace to cover Python's cancellation, settling and service-stop phases. SDK fit now reports elapsed operation time instead of accidentally subtracting a service-ownership boolean from the monotonic clock. Regression tests exercise the real fit result and malformed model packages; these limits do not imply authentication of a package or a proof that every third-party archive parser is defect-free.

Release and deployment

The release aligns Python/frontend metadata, the OpenAPI contract, generated API types and installation documentation at 2.4.0. Independent agent review is bound to the final PR commit, all required checks must pass, and merge commits record code contributors and reviewers. A human review is optional; the author cannot approve their own change.

Before publishing the GitHub release, download the Linux MATLAB toolbox artifact built by the successful MATLAB workflow on the release's main SHA. Attach OpenDPD-2.4.0.mltbx and its SHA256SUMS to the draft release, then publish that release to trigger the PyPI build. Verify the PyPI version and artifact hashes before promoting the hosted service.

Deploy the same reviewed main revision to the API, private GPU agent, pinned worker image and the public Pages build. Retain rollback source/image pins, finish queued/running work before switching, and check a real hosted PA-to-DPD workflow after activation. Publication and deployment results are recorded on the GitHub release. See Public Studio.