Beamforming locates sources, and measures the direction and speed of waves, from the recordings of an array of sensors. Six short notebooks show how to compute it in Python: first as plain loops that follow the formula, then with matrix products in numpy, pytorch and other libraries, for the common beamformers (Bartlett, cross-correlation, MVDR, MUSIC), and on field data. The code stays simple on purpose: a beamformer is about ten lines, and yours to adapt.
- Loops: what a beamformer computes, written as loops
- numpy: the same with array operations:
einsum, and matrix products, which are much faster - Other libraries: the same in pytorch, jax and numba, and how fast they all are
- Beamformers: Bartlett, cross-correlation, MVDR and MUSIC in code, what they can and cannot do, and how to scale beampower to a coherence between 0 and 1
- Field data, plane waves: the Gräfenberg array on the day of the 2011 Tohoku earthquake
- Field data, sources on a map: the 2023 Greenland landslide and seiche, recorded worldwide; first with a constant velocity, then with traveltimes from a velocity model, then with the radiation pattern of the source
Notebooks 1–3 compute exactly the same, on the same synthetic recordings; only the beamforming step differs. Notebooks 5 and 6 are complete examples to adapt to your own data. To run them, install the environment with uv (uv sync), or pip install numpy torch jax numba matplotlib obspy cartopy notebook.
All beamformers here have the same form: a matrix implementations/ has this in numpy, pytorch, jax, cupy and numba, in about 50 lines each. We timed them on one computer (2× Intel Xeon Gold 6326 with 32 cores, NVIDIA A40 GPU): one map of 10 000 grid points, from benchmarks/run.py measures your own computer.
Seconds, for 100 / 1000 sensors (– : not run, a smaller problem already took longer than a minute):
| cross-correlation, Bartlett | MVDR | MUSIC | |
|---|---|---|---|
| numpy, einsum | 243 / – | 244 / – | 241 / – |
| numpy | 5.0 / 105 | 7.2 / 162 | 7.2 / 303 |
| pytorch | 0.87 / 23 | 1.0 / 27 | 2.0 / 68 |
| jax | 7.6 / 59 | 26 / – | 6.7 / – |
| numba | 1.0 / 87 | 11 / 234 | 3.2 / – |
| pytorch, GPU | 0.18 / 4.1 | 0.67 / 23 | 3.2 / 46 |
| cupy, GPU | 0.20 / 4.4 | 0.65 / 24 | 3.2 / 46 |
| jax, GPU | 0.40 / 2.8 | 0.63 / 23 | 3.2 / 45 |
-
Write it as a matrix product: 50× faster than
einsumin numpy. - On a CPU, use pytorch: the fastest for every beamformer, 4–7× faster than numpy, for a few changed lines. numba's compiled loops keep up to about 100 sensors.
-
On a GPU, pytorch, cupy and jax are about equally fast: 5× faster than the CPU for cross-correlation. For MVDR and MUSIC with 1000 sensors the GPU gains little, because the inverse and the eigenvectors of
$K$ take most of the time. - The grid search grows with the number of sensors squared, the inverse and the eigenvectors with its cube.
All runs use single precision for the grid search, which is accurate enough for beamforming and can speed up computation dramatically.
The same synthetic recordings, beamformed with each tool as its documentation suggests, and with the pytorch code of notebook 3 (benchmarks/compare.py): plane waves on a 101 × 101 slowness grid, and sources on a 100 × 100 grid, for single time windows of 102 s, 7 min and 27 min. Each tool runs with its own defaults; some use all cores, some one. Lines end where a window took longer than 30 minutes or did not fit into memory.
All tools find the source, and where they use the same grid, their maps agree with ours (correlation 0.90–1.00). TwistPy finds the direction exactly, but its slowness is off by up to 56 %: it uses steering vectors at a single frequency. With 100 sensors, the ten lines of pytorch are 4–250× faster than ObsPy, TwistPy, acoular and covseisnet. beampower, a compiled delay-and-sum of the waveforms, grows only linearly with the number of sensors and is up to 3× faster than pytorch with 1000 of them; it computes the Bartlett beamformer only, which doesn't strictly require computing
How much memory does a problem need? Three arrays matter, each with 16 bytes per number (8 in single precision):
| array | size | 1 GB at |
|---|---|---|
| steering vectors of one frequency (and |
grid points × sensors | 67 000 grid points × 1000 sensors |
|
|
sensors² | 8000 sensors |
| spectra of all time windows | windows × sensors × frequencies | a week of 10-minute windows × 100 sensors × 539 frequencies |
So a laptop with 8 GB handles the benchmark problem above (160 MB per frequency), but not a 3D grid of 100³ points with 1000 sensors (16 GB of steering vectors per frequency), nor a month of windows from 1000 sensors (37 GB of spectra).
All three can be solved by doing one piece at a time; no extra library is needed. Loop over the frequencies (as the implementations do), over chunks of the grid, and keep the spectra on disk, stored with the frequency as first axis, so that one frequency is read at a time:
spectra = np.load("spectra.npy", mmap_mode="r") # (frequencies, sensors, windows), stays on disk
beampowers = np.zeros(len(gridpoints))
for w in range(len(omega)):
d = np.asarray(spectra[w]) # one frequency in memory: (sensors, windows)
K = d @ d.conj().T / d.shape[1]
np.fill_diagonal(K, 0)
for x in range(0, len(gridpoints), 10_000): # 10 000 grid points at a time
chunk = slice(x, x + 10_000)
traveltimes = np.linalg.norm(gridpoints[chunk, None] - stations[None], axis=2) / medium_velocity
s = np.exp(-1j * omega[w] * traveltimes)
beampowers[chunk] += np.sum((s.conj() @ K) * s, axis=1).realEarlier versions of this repository used dask for this. With the loops above, the pieces are already independent and small, and dask would only add a scheduler; it is worth it if you want to spread the pieces over several computers. What remains is
This is version 3. The earlier versions of this repository (releases v1 and v2; the notebooks of v2 are in archive/) wrote the beamformer as the match between two cross-spectral density matrices,
- writes the beamformer as usual,
$\mathbf{s}^H K, \mathbf{s}$ , with the steering vectors$\mathbf{s}$ .$S$ is never needed; neither by this nor by any other beamformer, including MVDR and MUSIC. That takes away most of the memory problem; for what remains, see "Large problems". - computes it with matrix products, frequency by frequency, and compares numerical libraries for exactly this computation, for four beamformers, on CPUs and GPUs (notebook 3,
benchmarks/run.py). - explains the beamformers, in code (notebook 4), and compares with other beamforming software (
benchmarks/compare.py). - condenses the plane-wave and field-data notebooks into one example (notebook 5), and replaces the geographic notebook by an example on the globe, with traveltimes from a velocity model (notebook 6).
Beamforming is a phase-matching algorithm commonly used to estimate the origin and local phase velocity of a wavefront propagating across an array of sensors. The most basic beamformer is the delay-and-sum beamformer, where recordings across the sensors are phase-shifted and summed (forming the beam) to test for the best-fitting source origin and medium velocity (Rost and Thomas, 2002).
The cross-correlation beamformer applies the same delay-and-sum idea to the correlation functions between all sensor pairs (Ruigrok et al. 2017). This has the advantage that only the coherent part of the wavefield is taken into account. In the frequency domain,
with
Because
The MVDR (minimum variance distortionless response, or Capon) beamformer uses the inverse of
It is the power that remains when the array is weighted to pass a wave from the candidate source unchanged, while letting through as little as possible of everything else. It separates sources that the Bartlett beamformer merges into one peak. The price:
MUSIC (multiple signal classification; Schmidt, 1986) splits the eigenvectors of
The steering vector of a true source is orthogonal to the noise subspace, so the denominator is close to zero there: a very sharp peak. MUSIC resolves close sources best, but the number of sources must be chosen, its output is not a power, and it is the most sensitive to errors in the model. Notebook 4 compares all four beamformers.
In seismology, "beamforming" is often synonymous with plane-wave beamforming. In plane-wave beamforming
with
When curved wavefronts are allowed, sources may be located within the sensor array and the grid that is tested is defined in space instead of the slowness-domain, adding at least one extra dimension. This is called matched field processing (e.g., Baggeroer et al. 1988). In practice, the difference between plane-wave beamforming and matched field processing lies in the computation of the steering vectors
In MFP, the travel time is computed as
with
Rost, S. & Thomas, C., 2002. Array seismology: Methods and applications. Reviews of Geophysics, 40, 2–1–2–27. doi:10.1029/2000RG000100
Ruigrok, E., Gibbons, S. & Wapenaar, K., 2017. Cross-correlation beamforming. J Seismol, 21, 495–508. doi:10.1007/s10950-016-9612-6
Baggeroer, A.B., Kuperman, W.A. & Schmidt, H., 1988. Matched field processing: Source localization in correlated noise as an optimum parameter estimation problem. The Journal of the Acoustical Society of America, 83, 571–587. doi:10.1121/1.396151
Svennevig, K., Hicks, S. P., Forbriger, T., Lecocq, T., Widmer-Schnidrig, R., Mangeney, A., et al., 2024. A rockslide-generated tsunami in a Greenland fjord rang Earth for 9 days. Science, 385, 1196–1205. doi:10.1126/science.adm9247
Capon, J., 1969. High-resolution frequency-wavenumber spectrum analysis. Proceedings of the IEEE, 57, 1408–1418. doi:10.1109/PROC.1969.7278
Schmidt, R., 1986. Multiple emitter location and signal parameter estimation. IEEE Transactions on Antennas and Propagation, 34, 276–280. doi:10.1109/TAP.1986.1143830
notebooks/ the six notebooks; download and traveltime scripts for notebook 6
implementations/ the same beamformer in different libraries
benchmarks/ speed measurements (run.py, compare.py) and figures (plot.py); results in results/
tests/ every implementation against the definition
archive/ the notebooks of the earlier versions (release v2)


