aethersdr / aethersdr/AetherSDR
[RFC] TX Linearity Analyzer — numeric IMD/shoulder/ACPR measurement from the radio's own transmission
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 221
- Forks
- 117
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 299
Description
Preflight
- I have read GOVERNANCE.md and confirmed this change requires an RFC
- I have searched existing issues and this RFC has not been proposed before
- I have not opened a PR for this change yet
Process note, stated up front: Phase 1 is already written, tested and pushed as
feat/tx-linearity-analyzer (commit 82dd5f24). No PR is open and none will be
until this RFC is decided. The implementation ran ahead of the RFC and that is the
wrong order — it is on the branch so the proposal below can be argued against real
code and real measurements rather than a sketch, but if this RFC is rejected or
substantially redirected the branch is disposable. Nothing has been merged.
Problem
A FlexRadio owner can enable SmartSignal (adaptive predistortion) and look at a
panadapter picture of their own transmission. That is the entire feedback loop
available to them today.
There is no number anywhere in it:
- no IMD3/IMD5 in dBc,
- no shoulder level, no ACPR,
- no AM-AM/AM-PM curve,
- and therefore no way to answer the question an operator with an amplifier
actually has — "is APD helping on this band, at this power, into this
amplifier, or not?"
Answering that today requires a spectrum analyzer. Most operators do not have one,
and the ones who do are not going to re-cable the shack to check a setting.
The hardware to answer it is already in the radio. A FLEX has published >80 dB
TX-to-RX port isolation, four DAX IQ channels, and its own two-tone test generator.
What is missing is the instrument that turns a capture of the transmitted signal
into published numbers with stated reference conventions and a stated noise floor.
There is a second, longer-range reason. The measurement half of a digital
predistorter is the same estimator as a linearity analyzer with the correction half
removed. Hermes-Lite 2 support (#78, feat/hl2-backend) will eventually want a
from-scratch PureSignal-equivalent, and that work is mostly DSP that has to be
right. Building the analyzer first, with the capture path behind an interface,
means the HL2 predistorter is a new capture adapter rather than a from-scratch DSP
effort — and it means the DSP arrives already validated against synthetic signals
with known answers.
Proposal
A TX Linearity Analyzer: a measurement instrument that captures the radio's own
transmitted signal through a receive-only port and puts real numbers on transmitter
and amplifier linearity.
Signal path
The operator routes attenuated TX energy into a receive-only port (RX A or XVTR),
tunes a slice there to the TX frequency, and points a panadapter at it. A DAX IQ
stream on that panadapter is the measurement. The radio's own two-tone generator
(TransmitModel::startTwoToneTune(), already in the tree) is the stimulus, so the
signal under test is exactly what a SmartSDR operator would produce by hand.
Everything downstream is arithmetic on complex baseband samples.
Three layers, dependencies one way
src/gui/ TxLinearityDialog instrument window
TxLinearitySpectrumView marked spectrum plot
|
v
src/core/txlinearity/ backend-agnostic measurement core.
No Qt in the DSP, no radio types, no Flex types.
^
|
ITxCaptureSource the seam. FlexDaxIqCapture (this phase),
HpsdrCapture (documented, not built)
ITxCaptureSource::hasReferenceChannel() is the capability that gates everything
downstream:
- false — FlexRadio. Reference-free measurements only. See "what this
deliberately does not do" below. - true — HPSDR/HL2. The radio supplies both the DAC reference stream and the RF
feedback stream, which enables true AM-AM/AM-PM and eventually correction.
The core degrades when there is no reference. It never fabricates one.
What it measures
- Welch-averaged spectrum, coherent-gain and ENBW corrected.
- IMD3 / IMD5 / IMD7, upper and lower, each with its frequency and level.
- Shoulder level over an explicit configurable band.
- ACPR over explicit configurable adjacent-channel bands.
- Tone imbalance, measured PEP, resolution, average count.
- The instrument's own noise floor, reported alongside every result. Any
product that does not clear it by a margin is flagged as unmeasurable and is
never quoted as a figure. A measurement that is silently reading the analyzer's
own noise — or the feedback receiver's own IMD — is worse than no measurement.
Three decisions are worth surfacing here because they are the ones that determine
whether the numbers are right:
Window. Default is a 7-term Blackman-Harris (~-92 dB sidelobes). A Hann
window's -31 dB skirt buries any product below roughly -50 dBc, and leakage is
coherent so no amount of averaging removes it — the window sets the instrument's
dynamic range outright. The window is selectable, but the default is pinned by a
regression test with a Hann control asserted to leak ≥20 dB more, so it cannot be
quietly "optimized" back.
Reference convention. For two equal tones, PEP sits exactly 6.02 dB above one
tone. Ham convention usually quotes IMD relative to one tone; ARRL lab
measurements are referenced to PEP, which reads 6 dB better for the identical
signal. Both are correct and a bare "IMD3 = -32 dB" is not. Every dBc value in
the UI, the table and the CSV carries both, labelled. PEP is computed from the
measured envelope rather than by subtracting the constant, so an imbalanced pair
reports the truth instead of the flattering idealisation.
Band integration. sum(|S[k]|²)/ENBW recovers a discrete tone's power and a
noise band's power alike (Parseval on the window). One expression behind tone
power, shoulder and ACPR, rather than three that can disagree.
Safety
A two-tone test is a high-duty-cycle, PEP-heavy transmission deliberately driven
towards the region where the amplifier misbehaves. It is the most hardware-hostile
thing this application can ask a radio to do, and it happens under the operator's
callsign.
The interlocks are a Qt-free, clock-injected policy object, so all of them are
unit-tested without a radio, without a GUI and without sleeping. An interlock that
can only be exercised by transmitting is an interlock that never gets exercised.
- Dummy-load / verified-clear confirmation required before every run, never
persisted, cleared when the run stops. - Hard transmit timeout, default 10 s, adjustable downward only without an
explicit override. It depends on nothing but the clock, so a radio that has gone
completely silent is still unkeyed at the deadline. - Duty-cycle cooldown between runs, with a visible countdown.
- SWR / PA-temperature / supply-voltage aborts from
MeterModel, with SWR gated on
real forward power (a reflected reading against near-zero forward is a ratio of
two noise samples and would abort every key-up). - Telemetry-stale abort, and immediate abort on loss of the capture stream.
- Band-plan refusal that fails closed: no plan loaded is not permission. The
occupied span is computed including order-7 products (3 tone-spacings beyond
each tone), and band-edge proximity warns explicitly. stopRun()unkeys as its first action and is idempotent; the destructor unkeys
too. Constitution Principle VI — every path fails closed.- The Run button is
markTxKeying(), so the automation bridge refuses it without
AETHER_AUTOMATION_ALLOW_TX, exactly like MOX / TUNE / ATU.
What this deliberately does not do
No predistortion on the FlexRadio backend, ever. It is architecturally
impossible from a client and worth stating so nobody attempts it later:
- DAX IQ is unidirectional. There is no API to send IQ to the radio for
transmission. The only inbound path is TX Audio, which carries audio. - An SSB modulator derives the analytic signal from a single real input via
Hilbert transform, so the envelope's phase is determined by its amplitude — an
arbitrary complex correction cannot be expressed in a real audio stream at all. - The TX filter sits downstream of injected audio. Correction works by generating
out-of-band products (3rd order at 3× bandwidth, 5th at 5×) that cancel the PA's
IMD; those are exactly what the passband filter removes. Anything generated
inside the passband is added distortion, not correction.
Also out of scope: modifying SmartSignal's behaviour, any HL2 correction path, and
remote/SmartLink operation over WAN.
Delivery phases
- Phase 1 — instrument skeleton and spectral measurements. (written, tested,
on the branch) Capture interface, Flex DAX IQ adapter, capture threading,
windowed spectral analysis, IMD/shoulder/ACPR, marked spectrum plot, numeric
readout, safety interlocks, full synthetic test suite, CSV/PNG export. Useful on
its own. - Phase 2 — synthesized-reference envelope analysis and A/B. AM-AM/AM-PM
reconstructed from the known two-tone stimulus (valid for that stimulus only),
and the headline workflow: capture with SmartSignal off, then on, overlaid with a
delta column. Nothing today does this. - Phase 3 — HPSDR capture adapter. True dual-stream reference/feedback,
sub-sample alignment with a diagnostic view, memory-polynomial fit. This is where
the analyzer becomes the front half of a predistorter.
Phases 2 and 3 are not scaffolded. Phase 1 is complete first, as it should be.
Cross-platform impact
No platform-specific code, and no new dependency on any platform.
- Linux: builds and runs today. Full test suite green (251/251), all CI static
checks clean, dialog verified rendering offscreen through the automation bridge. - macOS: no platform code involved. FFTW3 is already required (
brew install fftw) because vendored WDSP needs it, so nothing new to install. - Windows: same. FFTW3 already arrives via
scripts/setup/setup-fftw.ps1.
The only new external surface is FFTW3, which is already a hard requirement of
the tree — vendored WDSP will not build without it, and SpectralNR,
AudioEngine and Hl2Spectrum already link it. No find_package added, so no
.github/docker/Dockerfile change and no CI image rebuild.
The dialog uses PersistentDialog, so it inherits frameless-chrome handling and
geometry persistence on all three platforms with no per-platform work. Every
control carries accessible names/descriptions and the custom-painted plot carries a
spoken summary; tools/check_a11y.py is clean.
Feature availability is a radio capability question, not a platform one: it needs
DAX IQ and a receive-only port, so it is Flex-only in Phase 1 and HL2 gains it in
Phase 3.
Alternatives considered
Extend the existing panadapter instead of a separate instrument. Rejected.
SpectrumWidget is a GPU display path tuned for frame rate with a Hann window; a
measurement path needs a low-sidelobe window, power averaging across frames and
double precision throughout. Sharing them forces one to compromise, and the display
can afford to while the instrument cannot.
Put the DSP behind IRadioBackend as a radio capability. Rejected. The
analysis is arithmetic on complex samples and has nothing radio-specific in it;
putting it behind the seam would mean two copies when HL2 arrives. The capture
goes behind an interface; the measurement stays shared. This is the decision the
whole HL2 predistorter story depends on.
Port WDSP's PureSignal (third_party/wdsp/upstream/calcc.c) directly.
Rejected as an implementation, kept as an oracle. It is tightly coupled to
Protocol 1 assumptions — the 192 kHz dependency, the SetPk/GetPk peak
normalisation, the DDC channel mapping — none of which map onto a Flex DAX IQ
capture. Porting it would produce code that looks right and measures wrong. It
stays available as an independent implementation to check Phase 3's model fit
against, which is a luxury most DSP work does not get.
Golden test vectors as checked-in capture files. Rejected in favour of a
checked-in deterministic generator (tests/TxLinearityTestSignals.h, seeded
std::mt19937). A generator is reviewable — a reviewer can see the two-tone really
is two tones and the PA model really has the coefficients the assertions claim —
and it lets each test state its expected answer as a closed-form expression instead
of a magic number nobody can re-derive. Still hardware-free, still runs in CI.
Flagging it because it is a deliberate deviation from "vectors checked into the
repo".
A modal multi-page setup wizard. Rejected in favour of a guided setup section
inside the dialog. A wizard would be a new interaction pattern in an application
that has none, and the operator has to watch the live feedback level while adjusting
a physical attenuator — which a page-at-a-time wizard actively obstructs. The steps,
their order, and the refusal to run until each validates are unchanged.
Audio-domain predistortion on Flex. Rejected on the three architectural grounds
above. Recorded here so it is not re-attempted.
Implementation scope
New — measurement core (backend-agnostic, no Qt in the DSP):
src/core/txlinearity/TxLinearityTypes.h value types, reference conventions
src/core/txlinearity/SpectrumWindow.{h,cpp} windows + coherent gain + ENBW
src/core/txlinearity/TxSpectrumAnalyzer.{h,cpp} Welch averaging, peak interpolation
src/core/txlinearity/ImdAnalyzer.{h,cpp} IMD / shoulder / ACPR / noise floor
src/core/txlinearity/TxLinearitySafety.{h,cpp} interlocks (Qt-free, clock-injected)
src/core/txlinearity/ITxCaptureSource.h the seam
src/core/txlinearity/CaptureRing.h lock-free SPSC frame ring
src/core/txlinearity/FlexDaxIqCapture.{h,cpp} Flex adapter (no vendor-model dep)
src/core/txlinearity/TxLinearityController.{h,cpp} run orchestration + analysis thread
New — UI:
src/gui/TxLinearityDialog.{h,cpp} instrument window (PersistentDialog)
src/gui/TxLinearitySpectrumView.{h,cpp} marked spectrum plot
New — tests (all hardware-free, registered with add_test):
tests/TxLinearityTestSignals.h synthetic PA model + deterministic signals
tests/tx_linearity_spectrum_test.cpp window factors, leakage regression, integration
tests/tx_linearity_imd_test.cpp analytic IMD3/5, reference convention, noise floor
tests/tx_linearity_safety_test.cpp every interlock
Modified: CMakeLists.txt (source lists + 3 test targets), and one menu action
in src/gui/MainWindow_Menus.cpp.
New dependencies: none. FFTW3 is already required tree-wide.
Threading
Capture assembles frames on the thread delivering IQ (bounded memcpy into a
preallocated buffer — no allocation, no DSP), hands them to the analysis thread
through a lock-free SPSC ring, and results return by queued signal. The ring drops
newest on overflow rather than overwriting: a Welch average does not care which
frames it gets, only that each is internally contiguous, and overwriting a frame
mid-read would splice two capture instants together and manufacture spectral
content that was never transmitted. Dropped frames are counted.
This is a new thread, which is why the RFC list flags it as an architecture change.
Known risks
- The assumption the whole feature rests on is not yet verified against
hardware: whether DAX IQ actually flows during TX for a slice on a
receive-only port, or whether the receiver is muted. Verifying it needs a
deliberate on-air two-tone into a dummy load, which has not been run. If it
does not work, Phase 1 does not work — the DSP core and the safety layer would
still stand, but the Flex capture path would need rethinking. This should be
settled before any PR. - Feedback level calibration (the "too low / good / clipping" band) is derived
from SNR and headroom reasoning, not from measurements on a real feedback path.
Expect to tune it once (1) is answered. apd frequencyandapd sample_indexexist in FlexLib'sAPD.csbut are not
parsed by this tree. Not needed for Phase 1; likely wanted for Phase 2's A/B
view, and a small separate change.
Related
- #78 /
feat/hl2-backend— Hermes-Lite 2 support. Phase 3 is the bridge from this
analyzer to an HL2 predistorter. - #3849 (aetherd RFC) — the capture core takes no Qt and no radio types, so it sits
cleanly on the engine side of that split. The Flex adapter holds no vendor-model
pointer (EB3 ratchet clean, 0 blocking findings).
Clean-room note
third_party/wdsp is GPLv2-or-later and AetherSDR is GPLv3, so reading and
referencing it is licence-clean and Constitution Principle IV explicitly counts
open-source references as clean inputs. I checked whether any public-facing doc
claims provenance with no PowerSDR/Thetis lineage and found none — docs/ already
cites Thetis openly as a behavioural oracle. No documentation rescoping is
needed. Recorded here so the question is settled rather than re-litigated when
Phase 3 references calcc.c.
Asking for: a decision on the proposal, and specifically on (a) the Phase
1/2/3 split, (b) the menu placement (currently grouped with Radio Health and Slice
Troubleshooting — it measures the radio rather than configuring it, and the
Settings menu's placeholder-connect loop would hijack an action added there), and
(c) whether the hardware probe in risk 1 should gate the PR.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Read the RFC's src/core/txlinearity/ and src/gui/ layers, then inspect ITxCaptureSource and TransmitModel::startTwoToneTune(); Phase 1 is already on feat/tx-linearity-analyzer. Review the existing full synthetic test suite and tools/check_a11y.py; done means the RFC receives a decision, without expanding the work into Phases 2 or 3.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- backend, desktop, testing
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100