An independent, from-scratch replication (and platform for extending) of:
R. King, G. Huff, T. Bois, B. Campbell, "Experimental Investigation of Distributed Array Adaptive Beamforming for Interference Suppression in UAV Swarms", Drones 2026, 10, 253. doi:10.3390/drones10040253
A UAV swarm with one antenna per drone forms a motion-dynamic distributed random volumetric array. This package simulates such arrays and the three receive beamformers the paper studies — conventional, MVDR, and MMSE — under the realistic non-idealities of a swarm: imperfect element localization, non-uniform per-element noise, and non-uniform element orientations. The central finding, reproduced here, is that MMSE beamforming is robust to these non-idealities while MVDR is not.
This is a clean-room implementation written only from the paper's equations and descriptions; it does not use the authors' code or data.
The compiled paper (IEEE and single-column layouts) and the MP4 animations of the moving near-field swarm are attached to the latest release.
| Part | Paper sections | State |
|---|---|---|
| Simulation core (array model, 3 beamformers, 3 non-idealities) | §2–4 | ✅ implemented + validated |
| Figure/table reproduction | Figs 3, 6–13; Tables 1–2 | ✅ scripts in experiments/ |
| DSP receive chain + 3-state interference experiment (synthetic IQ) | §5–6 | ✅ implemented + validated |
| Extension: moving elements / weight tracking (block + RLS) | §7 future work | ✅ implemented |
| Extension: near-field beamforming | §7 future work | ✅ implemented |
| Extension: RLS + LCMV beamformers | §7 future work | ✅ implemented |
| Extension: closed-loop (decision-directed) RLS | §7 future work | ✅ implemented |
| Extension: per-element error model + latency sweeps | §7 future work | ✅ implemented |
| Extension: error-vs-ideal comparison animation | §7 future work | ✅ implemented |
Run with the package defaults (analytic / infinite-snapshot Wiener limit):
| Quantity | Paper | This codebase |
|---|---|---|
| MVDR SINR drop at σ = λ/100 (Fig 7) | ~30 dB | ~33 dB |
| SINR floor, 16 elem @ 20 dB SNR | 32 dB | 32.0 dB |
| Table 1 SINR (0/1/4/8 interferers) | 34.57 / 33.84 / 31.58 / 28.71 dB | 35.0 / 34.0 / 32.2 / 29.2 dB |
| Table 1 weight–SNR corr (0 interferers) | 0.9999 | 1.0000 |
| Table 2 SINR (0/1/4/8 interferers) | 27.21 / 26.83 / 25.70 / 23.67 dB | 27.4 / 26.8 / 26.4 / 25.0 dB |
| 3-state experiment Δ₃₂ (nulling gain) | 10.87 dB (anechoic) | 11.3 dB |
| 3-state experiment Δ₁₃ (residual loss) | 3.32 (anechoic) / 6.73 (lab) dB | 6.6 dB |
(Small differences trace to unstated details — exact aperture shape and the interferer-direction distribution — not to the algorithms. The synthetic DSP pipeline includes per-channel CFO/timing/phase-noise impairments, so its residual loss Δ₁₃ lands near the paper's multipath-lab value rather than the idealized anechoic one.)
A full write-up of the replication and all extensions is available in two
layouts — IEEE two-column and a single-column journal style — that share the
same section sources in
papers/distributed_beamforming/. Prebuilt
PDFs are attached to the
latest release;
to rebuild locally (requires MiKTeX):
python papers/build.py # regenerate figures, then build both layouts
python papers/build.py --no-figures # skip figure regeneration (faster)This writes ABF for Dynamic Random Arrays.pdf (IEEE, 8 pages) and
ABF for Dynamic Random Arrays (single-column).pdf into
papers/distributed_beamforming/.
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -e .import numpy as np
from dronebf import (ArrayGeometry, PlaneWave, MMSE, MVDR, Conventional,
receive, steering_vector, sinr_db, direction_from_angles)
rng = np.random.default_rng(0)
array = ArrayGeometry.random_volumetric(16, aperture=9.0, rng=rng) # 16 drones
soi = PlaneWave(direction_from_angles(0.0, 0.0)) # SOI from +z
intf = PlaneWave(direction_from_angles(0.7, 1.0)) # one interferer
data = receive(array, [soi, intf], powers=[1.0, 1.0], noise_power=0.01, rng=rng)
w_mmse = MMSE(data.reference).solve(data.X) # needs no geometry
w_mvdr = MVDR(steering_vector(array, soi.direction)).solve(data.X) # needs geometry
print(sinr_db(w_mmse, data.manifolds, data.powers, data.noise_power))python experiments/run_all.py --trials 300 # everything -> figures/
# or individually:
python experiments/fig06_07_position_error.py --trials 300
python experiments/table01_fig09_10_noise.py --trials 200
python experiments/fig16_21_interference.py --trials 15 # DSP 3-state experiment
# extensions (beyond the paper):
python experiments/ext_mobility.py --trials 300 # channel aging + update rate
python experiments/ext_rls_tracking.py --trials 40 # RLS tracking vs speed
python experiments/ext_nearfield.py --trials 300 # near-field mismatch
python experiments/ext_errors.py --trials 150 # per-element error sweeps
python experiments/ext_latency.py --trials 200 # reaction-latency sweep
python experiments/ext_rls_closed_loop.py --trials 40 # decision-directed RLS
python experiments/animate_swarm.py --frames 100 --fps 20 # moving swarm + moving sources MP4pytestdronebf/
geometry.py ArrayGeometry: random volumetric arrays, aperture control,
position error (§3.1), and an at_time() seam for moving elements.
elements.py Element patterns: isotropic, cos(theta), per-element random
broadsides (§3.3).
wavefront.py Source models: PlaneWave (far-field) and SphericalWave
(near-field) behind one Source interface — the near-field seam.
manifold() builds the steering/array-manifold vector.
channel.py receive(): the X = sum_k v_k s_k + noise data model, with
per-element noise (§3.2) and analytic-covariance helpers.
beamformers.py Conventional / MVDR (+ loading) / MMSE / LCMV / RLS / LMS
behind a Beamformer base class; a unified streaming_weights()
engine drives RLS & LMS in genie / decision-directed / soft /
pilot-periodic modes.
metrics.py Exact SINR, element SNR/SINR, array-SNR (Eq 7-9), gain patterns.
montecarlo.py Sweep runners that reproduce the figures and tables.
errors.py Unified per-element error model + parametric error sweeps.
mobility.py Per-element velocities for motion-dynamic swarms (extension).
tracking.py Channel-aging, block re-adaptation, RLS-tracking, and latency.
nearfield.py Near-field wavefront-model mismatch study (extension).
scenarios.py Combined near-field + moving + attitude-rotating swarm scene
with adaptive vs frozen weights (backs the animation).
dsp/ Synthetic-IQ receive chain (§5-6):
modulation.py BPSK + root-raised-cosine pulse shaping / matched filter.
impairments.py Per-SDR carrier offset, phase noise, fractional timing.
sync.py Coarse freq correction, Mueller-Muller timing, Costas loop,
and data-aided timing/carrier (robust at 0 dB interference).
equalize.py Lag alignment + least-squares linear equalizer.
pipeline.py Swarm IQ model, per-channel decode, 3-state experiment.
experiments/ One script per figure/table; writes PNGs to figures/.
tests/ Analytic regression tests + Monte-Carlo + DSP smoke tests.
- Units.
wavelengthdefaults to1.0, so positions, apertures (in λ²), and position-error σ (in λ) read directly off the figures. - Exact SINR. Because the simulator knows the true manifolds, powers, and
noise, SINR is computed exactly from the weights (
metrics.sinr) rather than estimated from samples — isolating beamformer behaviour from estimation noise. - Analytic vs sampled covariance. Monte-Carlo runners default to the
analytic (infinite-snapshot) covariance — the exact Wiener limit the paper
refers to — for fast, noise-free curves. Pass
snapshots=Mfor finite-sample behaviour (used by the DSP pipeline). - Sign convention.
PlaneWaveusesexp(+j k r·u), the exact far-field limit ofSphericalWave'sexp(-j k r), so far- and near-field manifolds are mutually consistent. This differs from the paper's Eq. 2 only by a global conjugation, which cancels in every SINR/gain/weight result. - Data-aided per-channel sync. The blind Mueller-Muller and Costas loops are implemented and tested (BER 0 through CFO/timing/phase-noise at high SNR), but the experiment pipeline uses data-aided timing/carrier recovery off the known BPSK header. At an equal-power (0 dB) interferer the decision-directed loops lose lock, whereas the header (uncorrelated with the interferer) gives ~27 dB of processing gain over 512 symbols — so the per-channel decode stays locked and the MMSE beamformer has coherent streams to combine. Coarse CFO is sub-bin refined (zero-pad + parabolic) so no fragile fine-frequency extrapolation is needed over the payload.
The paper assumes a static array per transmission. dronebf/mobility.py and
dronebf/tracking.py add time-varying geometry and answer two open questions
(python experiments/ext_mobility.py):
-
Channel aging — with weights frozen at t=0, SINR decays as the swarm drifts. Key finding: MMSE and MVDR decay identically (they are the same Wiener vector up to scale), so motion is not a localization-knowledge problem and MMSE's static-case robustness does not carry over. The 3-dB coherence distance is only ~0.035λ for the adaptive beamformers; conventional beamforming degrades far more slowly (~0.19λ) because it has no deep null to lose.
-
Re-adaptation rate — weights must be re-solved roughly every λ/30 of drift to stay within ~1 dB of optimal (a 0.1λ update interval already costs ~3.5 dB).
-
RLS tracking (
RLSbeamformer +tracking.sinr_vs_speed_tracking,python experiments/ext_rls_tracking.py) — the continuous-update counterpart to block re-adaptation. Streaming RLS recovers 20+ dB over frozen weights once the swarm moves, and the optimal forgetting factor tracks the speed (long memory wins at rest, short memory wins under fast motion). RLS converges to the MMSE/Wiener solution on stationary data. -
Closed-loop (decision-directed) RLS (
RLS(decision_directed=True, n_train=...)/rls_weights_decision_directed/tracking.rls_closed_loop_study,python experiments/ext_rls_closed_loop.py) — the fully realistic loop: no genie reference, just a short header then the receiver's own decisions fed back. It tracks a moving channel as well as the genie tracker using only a header (no pilot stream), but error propagation makes it collapse below a per-element-SNR threshold (~−3 to −5 dB here) where decisions become unreliable. Two mitigations lower that threshold: soft decisions (soft_bpsk_slicer, down-weighting uncertain symbols) and periodic pilots (pilot_period=, re-anchoring the loop). And anLMSbeamformer offers the same modes at O(N)/sample instead of RLS's O(N²) — cheaper on a power-limited node, with a higher steady-state error but comparable fast-motion tracking. All three are compared inext_rls_closed_loop.py, and the decision-directed streaming tracker is overlaid on the comparison animation (it rides with MMSE-ideal while the erred MVDR/LCMV collapse).
dronebf/nearfield.py (python experiments/ext_nearfield.py) drops the
plane-wave assumption using the SphericalWave source. With sources placed at a
range R relative to the Fraunhofer distance d_F = 2D²/λ:
- MMSE is immune to the wavefront model (flat ~32 dB at all ranges) because it never forms a steering vector — exactly mirroring its robustness to localization error.
- Plane-wave-steered MVDR degrades sharply for
R < d_F(down ~12 dB byR/d_F ≈ 0.2); near-field-aware MVDR (spherical steering) stays matched. - The takeaway: for swarms whose aperture is large enough that sources fall
inside
d_F, use MMSE or an explicit near-field steering vector.
dronebf/scenarios.py (MovingNearFieldScene) composes both extensions: a
near-field swarm whose elements drift in position and rotate in attitude, with
the MMSE weights re-solved every frame. experiments/animate_swarm.py renders it
as an MP4 dashboard (positions + attitudes in 3-D, the 2-D and 3-D beam
patterns, and adaptive-vs-frozen SINR over time):
python experiments/animate_swarm.py --frames 100 --fps 20 # -> figures/swarm_nearfield_moving.mp4The clip shows the adaptive beam holding its nulls on the near-field interferers
(~30 dB SINR) as the swarm moves, while the frozen-weight SINR collapses. MP4
encoding uses the pip-installed imageio-ffmpeg binary (no system ffmpeg needed).
dronebf/errors.py (ErrorModel, sinr_vs_error; python experiments/ext_errors.py)
sweeps each per-element error a real swarm suffers and reports MMSE / MVDR /
conventional SINR:
| Error | Units | Type | Effect |
|---|---|---|---|
position |
wavelengths | knowledge | MVDR collapses (reproduces Fig 7), MMSE flat |
attitude |
degrees | knowledge | MVDR degrades, MMSE flat |
timing |
carrier cycles | static hw | MVDR degrades, MMSE flat |
phase |
radians | static hw | MVDR degrades, MMSE flat |
gain |
dB | static hw | MVDR degrades, MMSE robust (often improves — SNR diversity) |
frequency |
cycles/snapshot | dynamic | degrades all, MMSE included |
(LCMV tracks MVDR on every axis — both are steering-based — so it collapses
under the same static errors despite giving the deepest nulls when error-free.)
The unifying result: MMSE is robust to everything static it doesn't have to
model (it folds position-independent, attitude, phase, timing, and gain errors
into its data-driven channel estimate); the only error that beats it is
frequency drift, which is time-varying and can't be absorbed into one weight
vector. ErrorModel composes (base= lets you hold several errors fixed while
sweeping one).
tracking.sinr_vs_latency (python experiments/ext_latency.py) models nodes
solving weights from the geometry known at t=0 but applying them only after a
latency tau, during which the swarm drifts and the targets move. Even MMSE
decays (channel aging), and swarm drift + source motion compound — quantifying
how fast nodes must react to geometry changes.
LCMV (linearly-constrained minimum variance) generalises MVDR: it pins the
SOI response while placing explicit deterministic nulls on each interferer
direction (C = [a_soi, a_int1, ...], f = [1, 0, ...]). Error-free it gives
the deepest, most controlled nulls — but it is the most knowledge-hungry
beamformer (it needs every interferer direction), so in the error sweeps it
collapses right alongside MVDR. It is included in ext_errors.py.
experiments/animate_comparison.py renders an MP4 contrasting error-free vs
error+latency beamforming as the swarm and sources move, with all three
adaptive methods:
python experiments/animate_comparison.py --frames 100 --fps 20
# -> figures/comparison_error_latency.mp4Each node solves weights from delayed (latency) and error-corrupted
(KnowledgeError: position + attitude) knowledge. The clip shows the ideal LCMV
nulls sitting on the interferers while the error+latency LCMV nulls slide off
target; the SINR panel shows MMSE staying high (it never used the corrupted
geometry — only latency bites) while MVDR and LCMV crater. This is the whole
project's thesis in one frame: data-driven beamforming is robust to everything
static it doesn't have to model.
- SMI / transmit beamforming — subclass
Beamformer; the covariance/cross-correlation helpers inbeamformers.pyare the building blocks (theRLSandLCMVclasses are worked examples of adding one).
This repository independently reproduces and extends a published paper. If you use it, please cite the original work:
R. King, G. Huff, T. Bois, B. Campbell, "Experimental Investigation of Distributed Array Adaptive Beamforming for Interference Suppression in UAV Swarms," Drones, vol. 10, no. 4, p. 253, 2026. doi:10.3390/drones10040253
You may additionally cite this implementation via the "Cite this repository"
button (GitHub reads CITATION.cff).
The code (dronebf/, experiments/, tests/) is licensed under the
BSD 3-Clause License — see LICENSE.
The paper and its figures (everything under papers/) are licensed under
Creative Commons Attribution 4.0 International (CC-BY-4.0):
https://creativecommons.org/licenses/by/4.0/.
This is an independent, clean-room reimplementation written from published equations. All credit for the original investigation and the Medusa testbed belongs to King, Huff, Bois, and Campbell (Penn State), whose open-access paper is reproduced and extended here.

