Skip to content

timcooke/DroneSwarmBeamforming

Repository files navigation

dronebf — Distributed-Array Adaptive Beamforming for UAV Swarms

License: BSD-3-Clause Python tests

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.

Showcase

Independent replication (Fig. 7) Synthetic-IQ interference experiment (Figs. 16/21)
MVDR collapses under localization error 3-state interference suppression
MVDR collapses ~33 dB at only λ/100 of positional error; MMSE (data-driven) is invariant — the paper's headline result. Re-nulling (▲) recovers the interferer-off SINR and rises above the y=x line, suppressing the correlated interferer.

The compiled paper (IEEE and single-column layouts) and the MP4 animations of the moving near-field swarm are attached to the latest release.


Status

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

Validation against the paper

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.)


Paper

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/.

Install

python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -e .

Quick start

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))

Reproduce the figures

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 MP4

Run the tests

pytest

Architecture

dronebf/
  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.

Design choices worth knowing

  • Units. wavelength defaults to 1.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=M for finite-sample behaviour (used by the DSP pipeline).
  • Sign convention. PlaneWave uses exp(+j k r·u), the exact far-field limit of SphericalWave's exp(-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.

Extension: moving elements / weight tracking (beyond the paper)

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 (RLS beamformer + 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 an LMS beamformer 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 in ext_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).

Extension: near-field beamforming (beyond the paper)

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 by R/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.

Combined near-field + moving + animation

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.mp4

The 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).

Extension: per-element error model + sweeps (beyond the paper)

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).

Extension: inter-node reaction latency (beyond the paper)

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.

Extension: LCMV + error-vs-ideal comparison animation (beyond the paper)

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.mp4

Each 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.

More extension seams (scaffolded)

  • SMI / transmit beamforming — subclass Beamformer; the covariance/cross-correlation helpers in beamformers.py are the building blocks (the RLS and LCMV classes are worked examples of adding one).

How to cite

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).

License / attribution

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.

About

Distributed-array adaptive beamforming for UAV swarms: independent clean-room replication of King et al. (Drones 2026) plus extensions (error model, tracking, near-field, LCMV, closed-loop RLS/LMS).

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors