OpenDPD ↔ MATLAB parity: the memory-polynomial DPD¶
Status: pre-registered; run 1 done — the memory-polynomial parity passes as registered (2026-10-09). Amendment 2 (below, 2026-10-09) registers a small additional comparison with rf.PAmemory; its outcome is recorded after the results of run 1. The registration (rules, data, MathWorks configuration, budgets
and the scored/diagnostic split) is committed before any comparison script exists or is run. The commits that
follow change only this status line, the run log and the Results section; they do not edit the registration. If
the registration itself must change, that is a dated amendment appended below, never an edit in place.
MATLAB users already trust comm.DPDCoefficientEstimator and comm.DPD. OpenDPD's mp_ls is the same model
class (memory polynomial, least squares, indirect learning). If the two agree up to the listed conventions, then
OpenDPD's baseline can be checked by anyone who has those functions, and a model fitted in OpenDPD (for example
on measured data in Studio) can be run by comm.DPD, which is what the planned opendpd-model-v1 export relies
on. This document fixes what is compared, with which MathWorks functions and which budgets, before any number
exists, and records the outcome afterwards, including everything that does not agree.
Rules¶
- Data, MathWorks configuration and budgets below are fixed before the first run. A run never changes them.
- The budgets are numerical-agreement budgets for two implementations of the same least-squares problem. They are loose compared with double-precision round-off (so that conditioning does not decide the verdict) and tight compared with any convention difference, which would produce differences of order one: 1·10⁻⁶ relative where conditioning or a single-precision interface is involved, 1·10⁻⁹ for double against double where neither is. They are not tuned on results.
- An item passes if its measured difference is within its budget. An item outside its budget is reported as failed with the numbers; the explanation is a hypothesis until it is shown (a diagnostic run is allowed and is reported next to the failure, never instead of it).
- If a comparison disagrees because the MATLAB-side code does not implement the registered procedure (a bug in the check, not a difference between the two tools), the MATLAB code is corrected to the registered procedure and the run is repeated. Every run, including the ones discarded, appears in the run log with the reason. MathWorks options are not changed after seeing a result.
- "Diagnostic" items are reported but have no budget and do not decide the verdict.
- A scored item that cannot be evaluated (a tool refuses or fails on it) is reported as not evaluable, never as agreement, and the verdict does not pass.
- MathWorks sample data is not used or redistributed. All data is generated by the scripts from the
repository's own waveform generator and the synthetic PA of
tests/fixtures/synthetic.py.
What this can and cannot show¶
- With the conventions below identical, both estimators solve the same overdetermined least-squares problem. Agreement on the registered cases means the basis (index order, lag, degree), the gain scaling, the row selection and the way the coefficients are applied are the same in the two tools, in the conditioning range of the registered cases. A disagreement means a convention differs or one implementation has a defect.
- It does not show that a memory polynomial is a good DPD for any particular amplifier, nor anything about measured data: the amplifier here is a synthetic memory polynomial.
- It does not cover rank-deficient or very ill-conditioned bases. The unscaled basis of the registered cases has
a condition number between about 4·10³ and 3·10⁵ (read from a Python-only exploration before registration,
see Data); a degree-9 case (about 3·10⁶ to 1·10⁷) was left out on purpose. OpenDPD's optional
rcondtruncation has no counterpart incomm.DPDCoefficientEstimator. - The OpenDPD side of the scored items calls the compute-core functions that the run service calls
(
opendpd.core.polynomial.mp_basis,fit_least_squares,PolynomialModel) on double-precision arrays. What the run machinery adds (float32 dataset storage, the gain taken from the train split, the zero-filled rows at every segment start) is quantified by the diagnostics and by the product-path item Q6, not hidden. - Not compared, with the reason: cross-term memory polynomial and OpenDPD's GMP — the bases differ
(a full cross-term within the memory window against lagging and leading envelope terms with their own
ranges), so there is no one-to-one comparison; recursive least squares — OpenDPD has no RLS fit;
MATLAB code generation,
singleprecision, fixed-point; every non-polynomial OpenDPD model (the GRU family has nocomm.*counterpart);rf.PAmemory— mapping a fitted PA model onto it belongs to the export work and is verified there.
Conventions, as read from the installed MATLAB code (R2026a Update 5)¶
These are statements about the code in toolbox/comm/comm/+comm/DPDCoefficientEstimator.m, DPD.m and
+internal/+dpd/dpdcore.m, not about the published documentation (which is not installed here and was not
consulted). They are the definitions this comparison uses.
| topic | MathWorks | OpenDPD mp_ls |
|---|---|---|
| model | Coef(i,d) multiplies x(n-(i-1)) · abs(x(n-(i-1)))^(d-1), Coef is MemoryDepth × Degree |
w[k·Q+q] multiplies x(n-q) · abs(x(n-q))^k, k = 0…K-1, q = 0…Q-1 |
| layout | Coef(:) (column-major) lists lag fastest, then degree |
w lists lag fastest, then degree: the same order, so Coef = reshape(w, Q, K) and Degree = K, MemoryDepth = Q |
| learning | PA_input ≈ Φ(PA_output / g) · Coef is fitted, Coef is then used as the predistorter (indirect learning) |
x ≈ Φ(y / G) · w is fitted on the measured train split and copied (indirect learning) |
| gain | PA_output is divided by 10^(DesiredAmplitudeGaindB/10) |
y is divided by G = max(abs(y)) / max(abs(x)) (set_target_gain) |
| rows | only rows with a complete memory (n ≥ MemoryDepth) |
delays are zero-filled and reset at every segment start (nperseg), so the first Q-1 rows of each segment are included |
| solver | xVec \ target (QR with column pivoting, no column scaling) |
column-normalised SVD least squares (numpy.linalg.lstsq), cutoff rcond, 0 = machine precision |
| application | zero initial delay state; output sample n uses x(n)…x(n-MemoryDepth+1) |
zero-filled delays at the start of a segment |
Consequences the comparison uses: DesiredAmplitudeGaindB is set to 10·log10(G) so that the two divisors are
equal (the property name reads like an amplitude in dB, but the code divides by 10^(g/10); diagnostic D3
shows what the other reading would do); the MATLAB estimator is compared with an OpenDPD fit restricted to the
rows with complete memory, and the zero-filled rows are evaluated separately (D2, D4).
Data¶
The signals of the LTE registration (docs/performance/matlab-parity.md, Signals), generated by the same
code: waveform ofdm-lte20-v1, seed 1, 10 subframes, interpolated ×4 to 122.88 MS/s (1 228 800 samples); x is
the PA input a · x_4 and y = memory_polynomial_pa(x) the PA output, in double precision (not rounded to
float32 unless stated), with a = 0.35 (S4) and a = 0.50 (S5). G = max(abs(y)) / max(abs(x)) over the whole
record: 0.5674 (S4) and 0.4030 (S5). Both sides receive identical arrays.
Four cases, each a (record, K, Q) combination:
| case | record | Degree K |
MemoryDepth Q |
unscaled condition number of the basis |
|---|---|---|---|---|
| C1 | S4 | 5 (MathWorks default) | 3 (MathWorks default) | 4.3·10³ |
| C2 | S4 | 7 | 5 | 1.4·10⁵ |
| C3 | S5 | 5 | 3 | 6.7·10³ |
| C4 | S5 | 7 | 5 | 3.1·10⁵ |
The condition numbers (2-norm, basis rows with complete memory, y/G) were computed in Python before this
registration to choose cases whose conditioning is moderate, so that the budgets below test the conventions
and not the arithmetic; no estimator of either tool had been run on this data at that time.
Constructed exact problem. For every case, c_true[k,q] = 0.6^(k+q) · (−1)^k · exp(j(0.35(k+1) + 0.6(q+1)))
(k = 0…K-1, q = 0…Q-1, a deliberately asymmetric set so that a transposed or reordered layout cannot
agree by accident). With v = y/G, the constructed input is x_syn[n] = Σ c_true[k,q] · v[n-q] · abs(v[n-q])^k,
zero before the start of the record, computed in NumPy by a direct double loop in the harness, independently of
mp_basis. The fit of x_syn from y has zero residual, so a correct estimator recovers c_true up to the
conditioning, with no residual-dependent term.
MathWorks configuration¶
est = comm.DPDCoefficientEstimator('PolynomialType','Memory polynomial', 'Algorithm','Least squares', ...
'Degree',K, 'MemoryDepth',Q, 'DesiredAmplitudeGaindB',10*log10(G));
coef = est(PA_input, PA_output); % PA_input = x (or x_syn), PA_output = y
dpd = comm.DPD('PolynomialType','Memory polynomial', 'Coefficients', C);
u = dpd(v);
Items¶
Scored, for every case C1–C4 (20 items):
| id | item | budget |
|---|---|---|
| Q1 | comm.DPDCoefficientEstimator on (x_syn, y): ‖Coef(:) − c_true‖ / ‖c_true‖ |
≤ 1·10⁻⁶ |
| Q2 | OpenDPD (mp_basis(y/G) rows Q-1…, fit_least_squares, rcond 0) on (x_syn, y): the same quantity |
≤ 1·10⁻⁶ |
| Q3 | comm.DPD with reshape(c_true, Q, K) applied to v = y/G, against x_syn: relative RMS difference |
≤ 1·10⁻⁹ |
| Q4 | The two predistorters on the measured-style data: comm.DPD with the coefficients the MATLAB estimator returned for (x, y), against mp_basis(x) @ w with the OpenDPD coefficients for (x, y) (rows with complete memory), both applied to x in double: relative RMS difference of the outputs |
≤ 1·10⁻⁶ |
| Q5 | The same coefficients applied by both tools through their real interfaces: comm.DPD with reshape(w_OpenDPD, Q, K) and the PolynomialModel module (float32 tensor in and out, one segment) applied to x rounded to float32: relative RMS difference |
≤ 1·10⁻⁶ |
Q2 is a numerical self-check of OpenDPD's fit on a problem with a known answer (the data is generated by an
independent implementation, but the solver is OpenDPD's). Q1 and Q3 carry the convention comparison: Q3 tests
that comm.DPD and OpenDPD's mp_basis define the same polynomial, Q1 that the MathWorks estimator returns
coefficients in OpenDPD's layout.
Scored, once (C1 data only, through the product path, product-path item Q6): the S4 pair is imported as a
dataset through opendpd.sdk (import_iq, float32 storage, nperseg = 2048), an mp_ls PA run and an
mp_ls DPD run with K = 5, Q = 3 are trained, and the coefficients in the DPD run's checkpoint are
compared with a fit computed independently from the run's own stored train split with segmented_basis
(nperseg 2048) and the gain set_target_gain returns for that split: relative difference ≤ 1·10⁻⁶.
Q6 checks that the run service does what the compute core does; it is the bridge between the scored items
(core functions) and the diagnostics D2 and D4 (run behaviour).
Diagnostic (no budget), for every case unless stated:
| id | item |
|---|---|
| D1 | Coefficient difference between the MATLAB estimator and OpenDPD on the measured-style data (x, y): ‖Coef(:) − w‖ / ‖w‖, with the unscaled condition number, the relative residual ρ = ‖r‖ / ‖Φ w‖ of the fit in dB, and the numerical rank OpenDPD reports |
| D2 | Boundary rows: OpenDPD's fit with the segmented basis (nperseg 2048, delays zero-filled and reset every segment, the basis fit_run builds) against the MATLAB estimator: relative coefficient difference and relative RMS difference of the predistorter outputs on x; and the relative residual of each fit in dB |
| D3 | The other reading of the gain setting: the MATLAB estimator with DesiredAmplitudeGaindB = 20·log10(G), against OpenDPD: relative coefficient difference |
| D4 | Product path (C1 data only): the MATLAB estimator on the run's stored train split with the run's gain, against the DPD run's checkpoint coefficients: relative coefficient difference; and OpenDPD's segmented_basis fit minus the full-memory fit on the same split, to show how much of it the zero-filled rows explain |
Verdict rule¶
The memory-polynomial parity passes only if every scored item (Q1–Q5 for C1–C4, and Q6) is within its budget. Otherwise it does not pass as registered, and the report says which items are outside, by how much, and what is known about why. The document draws no conclusion from the diagnostics except to describe them.
Reproduce¶
needs the Communications Toolbox. The record is docs/performance/matlab-parity-dpd.json; the Results block
below is generated from it. Exit status 1 means the parity did not pass as registered.
Run log¶
| run | date | change since the previous run | outcome |
|---|---|---|---|
| — | 2026-10-09 | plumbing checks on the first 60 000 samples of each record (--smoke; not the registered data, no report can be written from them) |
no registered number. They showed that the service refuses a least-squares baseline as a DPD run's PA reference, which led to amendment 1 |
| 1 | 2026-10-09 | none after amendment 1 | 21 of 21 scored items within budget. Record: matlab-parity-dpd.json |
| 2 | 2026-10-09 | amendment 2 only (items R1-R4; nothing of run 1 changes) | first attempt stopped by an error of the script, which looked for the fixture one folder too high (a bug of the check, not a result; no number was produced); second attempt: every item within budget, see Results of amendment 2 |
Amendment 1 (2026-10-09, before run 1)¶
The registration of Q6 says that an mp_ls PA run and an mp_ls DPD run are trained. The service refuses a
least-squares baseline as the PA reference of a DPD run ("a PA-modeling reference, not a DPD surrogate"), which a
plumbing check on truncated data (60 000 samples per record, not the registered data, no report written) showed
before any registered run. The harness therefore trains a small gradient-trained GRU surrogate (hidden_size 6, one
epoch, CPU) as the DPD run's PA reference; the DPD run is the mp_ls run with K = 5, Q = 3 as registered. The
platform identifies the postdistorter on measured data and uses the surrogate only for evaluation, so the
surrogate does not enter the coefficients that Q6 and D4 compare. No item, budget or case changes.
Amendment 2 (2026-10-09, before run 2): rf.PAmemory¶
The registration above lists rf.PAmemory among the things not compared and says that mapping a fitted memory-polynomial
model onto it belongs to the export work. This amendment registers that comparison. It adds items; it changes none of the
items, budgets, data or conclusions of run 1. Run 2 is the first and only run of the items below.
What is known before registering (disclosed). An unregistered exploration on one random signal and on the mp_ls
fixture of the toolbox's tests (K = 3, Q = 4) showed that rf.PAmemory(Model='Memory polynomial', CoefficientMatrix=C) with
the comm.DPD coefficient layout C = reshape(w, Q, K) agrees with comm.DPD from the Q-th output sample on, and that its
first Q − 1 outputs agree with comm.DPD run on the signal preceded by Q − 1 copies of its first sample (largest difference
2.5·10⁻¹⁶). That is: the delay line of rf.PAmemory starts filled with the first input sample, not with zeros. The items
below test this as a hypothesis on more cases; they were not tuned to results.
Data. Seeded (rng seeds below), generated by the script; no MathWorks sample data. For every pair (Q, K) with
Q ∈ {1, 2, 4, 7} and K ∈ {1, 3, 5}: a complex coefficient matrix C (Q-by-K, real and imaginary parts from randn, scaled by
1/(QK)) and, for each signal seed s ∈ {11, 12, 13}, a complex row-independent test signal of 256 samples (0.3·(randn + 1j·randn)).
That is 36 cases. Item R3 uses the mp_ls-dpd package of Matlab/toolbox/tests/data and the same three signal seeds with 128
samples (its segment length).
Items (all scored; budgets are for double against double, expected round-off 10⁻¹⁶; relative means divided by the largest magnitude of the reference output).
| Item | Comparison | Budget |
|---|---|---|
| R1 | rf.PAmemory(C) applied to [zeros(Q-1,1); x], outputs Q to the end, against comm.DPD(C) applied to x (zero initial state), all 36 cases |
1·10⁻¹² relative |
| R2 | The first Q − 1 outputs of rf.PAmemory(C) applied to x alone against the first Q − 1 outputs of comm.DPD(C) applied to x preceded by Q − 1 copies of x(1) (the cases with Q ≥ 2: 27 cases) |
1·10⁻¹² relative |
| R3 | opendpd.apply(model, x) of the mp_ls-dpd fixture (one 128-sample segment, so a zero state at the start, single-precision output) against rf.PAmemory(C) with C = model.commCoefficients() applied to [zeros(Q-1,1); x], outputs Q to the end, 3 signals |
1·10⁻⁶ relative (the single-precision interface, as for Q5) |
| R4 | rf.PAmemory(C) applied to x(1:128) and then, on the same object, to x(129:256), against one call on all of x (after the pad, i.e. the delay line carries across calls), 36 cases, outputs 129 to 256 |
1·10⁻¹² relative |
Verdict rule. The mapping passes only if every item is within its budget; otherwise the report says which are outside, by how much, and what is known about why.
What this can and cannot show. It shows how the rf.PAmemory System object of this MATLAB release treats the
coefficient matrix and its initial delay line, for the memory-polynomial model with the sizes above. It does not cover the RF
Blockset amplifier block (a Simulink block that may be implemented differently), the other nonlinearity models of
rf.PAmemory (cross-term, generalised memory polynomial), the UnitDelay property at values other than its default, other
MATLAB releases, or anything about an amplifier. Reproduce: parityPAmemory in Matlab/toolbox/examples/parity (needs RF
Toolbox and Communications Toolbox).
Results¶
Verdict: the registered memory-polynomial parity passes — 21 of 21 scored items are within budget, with
margins of several orders of magnitude (the largest scored difference relative to its budget is Q5, 2.5·10⁻⁸
against 1·10⁻⁶, which is the float32 interface). Run on MATLAB R2026a Update 5 (Communications Toolbox 26.1),
Linux. The working tree had uncommitted changes in documentation files under Matlab/toolbox only; no code
under test, no script and no .m file was modified.
What the scored items show:
- The two tools define the same polynomial.
comm.DPDwith the true coefficients reproduces the independently constructed input to 6·10⁻¹⁶ relative (Q3), in all four cases. - Both estimators recover a known answer in the same layout. From the constructed exact problem the MathWorks
estimator returns
c_truewith relative error at most 4.7·10⁻¹³ (Q1) and OpenDPD's fit at most 8.9·10⁻¹⁴ (Q2), including the cases whose unscaled condition number is 1.4·10⁵ and 3.1·10⁵. The layout isCoef = reshape(w, Q, K)withDegree = KandMemoryDepth = Q; an asymmetricc_truerules out a transposed or reordered layout agreeing by accident. - On the plant data they give the same predistorter. With the complete-memory rows and
DesiredAmplitudeGaindB = 10·log10(G), the coefficient vectors agree to at most 3.9·10⁻¹³ (D1) and the predistorter outputs to at most 4.8·10⁻¹³ relative (Q4). comm.DPDcan run OpenDPD's coefficients. Through thePolynomialModelmodule's float32 interface the outputs differ by 2.5·10⁻⁸ relative (Q5), which is float32 rounding.- The run service stores what the compute core computes. The coefficients in the checkpoint of the SDK-trained
mp_lsDPD run are identical to the fit recomputed from the run's stored train split (Q6: difference 0).
What the diagnostics add (no budget, so no verdict, but they are the practical content for a MATLAB user):
- The gain setting must be
10·log10(G). ReadingDesiredAmplitudeGaindBas20·log10(G)(D3) gives coefficients that differ from OpenDPD's by 83 % to 97 %. The estimator dividesPA_outputby10^(DesiredAmplitudeGaindB/10); to reproduce OpenDPD'sG = max|y| / max|x|the property is10·log10(G). - An OpenDPD run and the MathWorks estimator differ by the zero-filled rows, and by nothing else we could
find. OpenDPD includes the first
Q-1rows of everynpersegsegment, with delays filled by zeros, because the model is evaluated with exactly that state reset; MathWorks uses only rows with complete memory (D2). Withnperseg= 2048 the coefficient vectors differ by 0.6 % and 0.9 % forK = 5,Q = 3, and by 11 % and 19 % forK = 7,Q = 5, where the basis is worse conditioned; the predistorter outputs on the same signal differ by 0.12 % to 0.70 % relative (0.03 % to 0.54 % when the segmented coefficients are applied continuously). The residual of the segmented fit on the complete-memory rows is at most 0.11 dB worse than that of the complete-memory fit (below 0.01 dB forK = 5,Q = 3on S5). Through the product path (D4,K = 5,Q = 3) the MathWorks estimator differs from the checkpoint by 0.624 %, and the segmented fit differs from the complete-memory fit by 0.623 % on the same split: the difference is accounted for by the zero-filled rows.
Limits of this evidence:
- One MATLAB release on one platform; a synthetic memory-polynomial amplifier; degrees up to 7 and memory depths
up to 5, in a conditioning range of about 4·10³ to 3·10⁵. Nothing here says how the two behave on a rank-deficient
or much worse conditioned basis, where OpenDPD's column scaling and
rcondcutoff and MathWorks' unscaled QR solve would be expected to diverge. - The scored items of the OpenDPD side call the compute-core functions the run service calls. Q6 ties them to one SDK-trained run; it does not test every model configuration the service accepts.
- Only the memory polynomial is compared. OpenDPD's GMP and the cross-term memory polynomial of MathWorks have
different bases; the GRU family has no
comm.*counterpart.
What this run does not do: it does not change how mp_ls is fitted. Whether to offer a complete-memory-rows
option for MATLAB-comparable coefficients is a decision for the maintainers; it would not change the frozen
Arena baselines, which are fitted as they are.
Reproduce: python scripts/matlab_parity_dpd.py --work /tmp/parity-dpd --matlab <path to matlab> --write-report
(about two minutes; needs the Communications Toolbox and an OpenDPD checkout). Exit status 1 means the parity did
not pass as registered.
Run 1 — 2026-10-09. MATLAB 26.1.0.3346908 (R2026a) Update 5; Communications Toolbox 26.1. Python 3.13.14, NumPy 2.4.4, SciPy 1.18.0, PyTorch 2.13.0+cu132. OpenDPD commit cc5b90792613 with local modifications.
Scored items: 21 of 21 within budget, 0 outside budget, 0 not evaluable — the registered memory-polynomial parity passes.
Scored items¶
| id | item | difference | budget | within |
|---|---|---|---|---|
| Q1 | C1 (S4, K=5, Q=3): MATLAB estimator on the exact problem vs c_true | 1.779e-14 | 1e-06 | yes |
| Q2 | C1 (S4, K=5, Q=3): OpenDPD fit on the exact problem vs c_true | 5.46e-15 | 1e-06 | yes |
| Q3 | C1 (S4, K=5, Q=3): comm.DPD(c_true) on y/G vs the constructed input | 2.263e-16 | 1e-09 | yes |
| Q4 | C1 (S4, K=5, Q=3): predistorter outputs, each tool with its own coefficients | 1.554e-14 | 1e-06 | yes |
| Q5 | C1 (S4, K=5, Q=3): the same coefficients applied by comm.DPD and by the PolynomialModel module (float32) | 2.535e-08 | 1e-06 | yes |
| Q1 | C2 (S4, K=7, Q=5): MATLAB estimator on the exact problem vs c_true | 4.745e-13 | 1e-06 | yes |
| Q2 | C2 (S4, K=7, Q=5): OpenDPD fit on the exact problem vs c_true | 8.316e-14 | 1e-06 | yes |
| Q3 | C2 (S4, K=7, Q=5): comm.DPD(c_true) on y/G vs the constructed input | 3.179e-16 | 1e-09 | yes |
| Q4 | C2 (S4, K=7, Q=5): predistorter outputs, each tool with its own coefficients | 2.956e-14 | 1e-06 | yes |
| Q5 | C2 (S4, K=7, Q=5): the same coefficients applied by comm.DPD and by the PolynomialModel module (float32) | 2.537e-08 | 1e-06 | yes |
| Q1 | C3 (S5, K=5, Q=3): MATLAB estimator on the exact problem vs c_true | 3.164e-14 | 1e-06 | yes |
| Q2 | C3 (S5, K=5, Q=3): OpenDPD fit on the exact problem vs c_true | 1.211e-14 | 1e-06 | yes |
| Q3 | C3 (S5, K=5, Q=3): comm.DPD(c_true) on y/G vs the constructed input | 3.335e-16 | 1e-09 | yes |
| Q4 | C3 (S5, K=5, Q=3): predistorter outputs, each tool with its own coefficients | 2.556e-14 | 1e-06 | yes |
| Q5 | C3 (S5, K=5, Q=3): the same coefficients applied by comm.DPD and by the PolynomialModel module (float32) | 2.531e-08 | 1e-06 | yes |
| Q1 | C4 (S5, K=7, Q=5): MATLAB estimator on the exact problem vs c_true | 1.246e-13 | 1e-06 | yes |
| Q2 | C4 (S5, K=7, Q=5): OpenDPD fit on the exact problem vs c_true | 8.935e-14 | 1e-06 | yes |
| Q3 | C4 (S5, K=7, Q=5): comm.DPD(c_true) on y/G vs the constructed input | 5.918e-16 | 1e-09 | yes |
| Q4 | C4 (S5, K=7, Q=5): predistorter outputs, each tool with its own coefficients | 4.76e-13 | 1e-06 | yes |
| Q5 | C4 (S5, K=7, Q=5): the same coefficients applied by comm.DPD and by the PolynomialModel module (float32) | 2.537e-08 | 1e-06 | yes |
| Q6 | C1 data through the SDK: DPD run checkpoint vs the segmented fit recomputed from the run's train split | 0 | 1e-06 | yes |
The difference of Q1–Q6 is relative: ‖a − b‖ / ‖b‖ over the flattened complex vectors.
Diagnostics (no budget)¶
| id | item | value | note |
|---|---|---|---|
| D1 | C1 (S4, K=5, Q=3): coefficients, MATLAB estimator vs OpenDPD (complete-memory rows) | 9.527e-15 | unscaled condition number 4.27e+03; OpenDPD fit residual -44.69 dB; rank 15 |
| D2 | C1 (S4, K=5, Q=3): coefficients, MATLAB estimator vs OpenDPD segmented fit (nperseg 2048) | 0.005697 | residual on complete-memory rows (dB): MATLAB -44.69, OpenDPD complete-memory -44.69, OpenDPD segmented -44.68 |
| D2 | C1 (S4, K=5, Q=3): predistorter outputs, MATLAB vs OpenDPD segmented fit applied per segment | 0.001221 | applied continuously instead: 0.000314 |
| D3 | C1 (S4, K=5, Q=3): coefficients with DesiredAmplitudeGaindB = 20 log10(G) vs OpenDPD | 0.8269 | — |
| D1 | C2 (S4, K=7, Q=5): coefficients, MATLAB estimator vs OpenDPD (complete-memory rows) | 1.702e-13 | unscaled condition number 1.37e+05; OpenDPD fit residual -47.96 dB; rank 35 |
| D2 | C2 (S4, K=7, Q=5): coefficients, MATLAB estimator vs OpenDPD segmented fit (nperseg 2048) | 0.111 | residual on complete-memory rows (dB): MATLAB -47.96, OpenDPD complete-memory -47.96, OpenDPD segmented -47.87 |
| D2 | C2 (S4, K=7, Q=5): predistorter outputs, MATLAB vs OpenDPD segmented fit applied per segment | 0.001357 | applied continuously instead: 0.000774 |
| D3 | C2 (S4, K=7, Q=5): coefficients with DesiredAmplitudeGaindB = 20 log10(G) vs OpenDPD | 0.93 | — |
| D1 | C3 (S5, K=5, Q=3): coefficients, MATLAB estimator vs OpenDPD (complete-memory rows) | 1.494e-14 | unscaled condition number 6.65e+03; OpenDPD fit residual -27.11 dB; rank 15 |
| D2 | C3 (S5, K=5, Q=3): coefficients, MATLAB estimator vs OpenDPD segmented fit (nperseg 2048) | 0.008705 | residual on complete-memory rows (dB): MATLAB -27.11, OpenDPD complete-memory -27.11, OpenDPD segmented -27.11 |
| D2 | C3 (S5, K=5, Q=3): predistorter outputs, MATLAB vs OpenDPD segmented fit applied per segment | 0.002114 | applied continuously instead: 0.000776 |
| D3 | C3 (S5, K=5, Q=3): coefficients with DesiredAmplitudeGaindB = 20 log10(G) vs OpenDPD | 0.9166 | — |
| D1 | C4 (S5, K=7, Q=5): coefficients, MATLAB estimator vs OpenDPD (complete-memory rows) | 3.925e-13 | unscaled condition number 3.08e+05; OpenDPD fit residual -28.00 dB; rank 35 |
| D2 | C4 (S5, K=7, Q=5): coefficients, MATLAB estimator vs OpenDPD segmented fit (nperseg 2048) | 0.1852 | residual on complete-memory rows (dB): MATLAB -28.00, OpenDPD complete-memory -28.00, OpenDPD segmented -27.89 |
| D2 | C4 (S5, K=7, Q=5): predistorter outputs, MATLAB vs OpenDPD segmented fit applied per segment | 0.007003 | applied continuously instead: 0.00541 |
| D3 | C4 (S5, K=7, Q=5): coefficients with DesiredAmplitudeGaindB = 20 log10(G) vs OpenDPD | 0.9712 | — |
| D4 | MATLAB estimator on the run's train split vs the checkpoint | 0.00624 | — |
| D4 | segmented fit minus complete-memory fit on the same split (what the zero-filled rows change) | 0.006233 | — |
Cases¶
| case | record | K | Q | G | unscaled condition number | OpenDPD fit residual (dB) | rank |
|---|---|---|---|---|---|---|---|
| C1 | S4 | 5 | 3 | 0.567423 | 4.27e+03 | -44.69 | 15 |
| C2 | S4 | 7 | 5 | 0.567423 | 1.37e+05 | -47.96 | 35 |
| C3 | S5 | 5 | 3 | 0.403027 | 6.65e+03 | -27.11 | 15 |
| C4 | S5 | 7 | 5 | 0.403027 | 3.08e+05 | -28.00 | 35 |
Results of amendment 2: rf.PAmemory (run 2, 2026-10-09)¶
Verdict: every registered item is within its budget on MATLAB R2026a Update 5 (RF Toolbox 26.1, Communications Toolbox
26.1), Linux, from parityPAmemory in Matlab/toolbox/examples/parity (it prints this table and computes the verdict from the
registered budgets).
| Item | Cases | Worst relative difference (case) | Budget |
|---|---|---|---|
R1 rf.PAmemory on [zeros(Q-1,1); x] against comm.DPD on x |
36 | 4.2·10⁻¹⁶ (Q = 4, K = 5, seed 13) | 1·10⁻¹² |
R2 first Q − 1 outputs against comm.DPD on x preceded by Q − 1 copies of x(1) |
27 | 1.3·10⁻¹⁶ (Q = 7, K = 1, seed 12) | 1·10⁻¹² |
R3 opendpd.apply of the mp_ls-dpd fixture against rf.PAmemory with commCoefficients() |
3 | 4.5·10⁻⁸ (seed 11) | 1·10⁻⁶ |
| R4 a second call on the same object against one call on the whole signal | 36 | 0 (all 36 identical) | 1·10⁻¹² |
What this shows, for the rf.PAmemory System object of this release and the memory-polynomial model: the coefficient matrix has the
layout of comm.DPD, so the matrix OpenDPD's commCoefficients() returns can be given to rf.PAmemory unchanged (R1, R3); the
delay line of rf.PAmemory starts filled with the first input sample, not with zeros, which is the only difference from
comm.DPD and from OpenDPD's evaluation of a segment (R2); to reproduce OpenDPD's zero-history segment, prepend Q − 1 zeros and
drop the first Q − 1 outputs (R1, R3); and the delay line carries across calls, so a stream can be fed in chunks (R4).
What it does not show: the RF Blockset amplifier block, the other nonlinearity models of rf.PAmemory, the UnitDelay
property at values other than its default, other releases, or anything about an amplifier. Two notes on the registration, made here
and not edited into it: the phrase "complex row-independent test signal" means independent identically distributed complex
Gaussian samples (0.3·(randn + 1j·randn)), and the registration did not fix how the coefficient matrix is drawn, so the script
draws it from the seed 1000 + 10·Q + K; neither was changed after seeing a number.