PA Characterization Bench
A browser-driven RF power-amplifier test bench. It runs a vector signal generator, a signal analyzer and a 4-channel supply over raw SCPI, and turns a morning of manual measurements into one button per test.

Characterizing a power amplifier means coordinating three instruments, sequencing bias in an order that won’t destroy the part, and calibrating two cable paths. Every step is easy to get silently wrong: mix up total and per-carrier power and you’re off by 10.79 dB, or leave the analyzer on the previous test’s frequency and it measures the noise floor. I wanted a bench where every number is referred to the DUT and every report traces back to the hardware that produced it.
What it measures
- Gain and compression: small-signal gain, P1dB, P3dB, Psat, PAE and drain efficiency, with drain current logged at every step. CW or modulated drive.
- Two-tone linearity: IM3, IM5, OIP3, IIP3 and OIP5. It fits OIP3 only over the longest run where it is actually constant, the only region where the number means anything.
- Multicarrier: up to 12 QPSK carriers, with per-carrier EVM, ACPR and flatness, repeated at the low, mid and high end of the band.
- Bias sequencing: gate to pinch-off, drain up, then bisect the gate until drain current hits target. Bring-down reverses it, and a failure part-way through unwinds whatever was already applied.
- Self-test and loopback: probes every SCPI command the app depends on, then validates carrier placement, level, demodulator lock and EVM floor with the generator cabled straight to the analyzer before the DUT goes in.
- Reports: a printable report and PDF carrying DUT identity, bias, stimulus, calibration state and each instrument’s
*IDN?, plus CSV and a session file with the full SCPI log.
Decisions an RF engineer will recognize
Analyzer reference level and attenuation are one decision. A sweep predicts its top output level, sets attenuation so that level lands at the mixer’s −10 dBm operating point, and holds it for the whole curve. A 2 dB attenuator step half-way up a compression sweep looks exactly like the PA degrading.
Per-carrier ACPR is measured in the guard band. On a contiguous carrier plan the textbook adjacent channel is the next carrier and reads about 0 dBc. The app measures the 0.668 MHz guard between carriers instead, and says plainly that the figure compares parts rather than meeting a mask.
The noise sweep holds total power constant, so the amplifier’s compression state stays put while EVM moves. The generator has no hardware noise option, so noise is added in software, referenced to symbol rate, and verified to land within 0.06 dB of the commanded C/N.
The simulator works at the SCPI level. One physical PA model (a ~6 W GaN class-AB stage) drives all three simulated instruments, so P1dB, OIP3, ACPR, EVM and PAE come from the same physics. Mock and real hardware run the same adapter code. The images on this page come from the simulator.
Debugging, the real kind
The first hardware bring-up commit is titled “seven defects the simulator could not show.” Two favorites:
- 16–28% EVM on a clean amplifier. Channel-power reads inherited their center frequency from the previous test, so auto-ranging saw only noise, chose 0 dB attenuation, and overloaded the analyzer’s ADC. Center frequency became a required argument, plus a regression test that replays the SCPI log and checks every read was taken where the generator was transmitting.
- 56% EVM on every carrier with correct power, gain and spectrum. Noise from an earlier sweep had been saved into the waveform file. Found by demodulating the analyzer’s own I/Q and splitting the error vector into radial and tangential parts: equal parts means additive noise. Now every measurement refuses to start while injected noise is set.
How it was built
Every one of the 71 commits was written with Claude Code. What made that work was treating the agent like a new engineer on a flight program:
- Hard invariants: seven rules that must never break, including the RF and bias interlocks, depletion-mode bias order, all derived math in one module, and mocking only at the SCPI layer.
- Verified commands only: each SCPI command is recorded with the manual page that confirms it. Unverified analyzer commands live in one table with the rule “fix the dict, never the code.”
- Independent test harnesses:
check_math.pyrebuilds 107 expectations from first principles and never calls the implementation’s own helpers. Other harnesses replay the SCPI log as a regression test and drive the real UI with Playwright against the simulator.