Two physics engines that check each other.

A sonar simulator is easy to write and hard to trust. The usual failure is a renderer that produces plausible-looking speckle with no defensible relationship to acoustics. ApertureLab carries two independent forward models built on different mathematics, writing the same output format, and holds them against each other and against a published result.

Backend one

Point scatterer: brute force, conceptually exact.

The seafloor and every object on it become millions of individual reflectors. For each transmitted pulse, and for every reflector and receive element, the GPU computes the travel time, the returned amplitude, and whether anything blocks the line of sight, then sums those echoes into a raw time series.

Amplitude follows a Lambert scattering law modulated by the transducer beam pattern and seawater absorption (Thorp, per Urick Eq. 5.18); the scattering model implements PoSSM (Brown et al., 2017 and 2019). Because it tracks individual reflectors, this backend handles anything: curved tracks, circular collections, arbitrary geometry, six-degree-of-freedom platform motion.

Implementation: simulator/gpu_raytrace.py, simulator/gpu_echo.py.

A 3D rendering of a yellow torpedo-shaped underwater vehicle flying above a brown simulated seafloor scattered with objects such as barrels, a small boat, a pipeline, and a rock pile. A translucent cyan wedge of sound radiates from the vehicle's starboard side down to the seafloor, with expanding wavefront arcs inside it, and a cyan outline traces the long narrow footprint the beam illuminates across the swath.
One ping's transmit beam and its seafloor footprint, computed from the real array geometry; the vehicle's motion sweeps this strip along the track, and it is this illuminated patch the forward model integrates over.
580,398,843
scatterers in one 71.5 by 100 m scene at 80,000 per square metre, 380 pings, 32 channels
.bench_clean.log, 2026-09-13
226 s → 19 s
same scene at 10,000 per square metre after the 2026-09 optimization pass; the resulting image correlates 0.9998 with the old path
docs/reference/simulator.md
0.999998
correlation between the FFT echo path and its predecessor through the full pipeline, at 96 to 124 times the speed
simulator/gpu_echo.py

Backend two

Fourier wavefield: cost independent of scene complexity.

Instead of tracking reflectors one at a time, the Fourier-wavefield backend renders the scene once as a depth image, converts that picture into a spatial-frequency spectrum, and applies one exact change of variables, the Stolt mapping, to turn what the scene looks like into what the sonar would have recorded.

Its cost does not depend on the number of scatterers, so it wins on very dense scenes where the point-scatterer method is counting grains of sand. It implements Sanford, Thomas and Hunter, IEEE Journal of Oceanic Engineering 49(4):1501-1515, 2024, in the paper's four steps.

Implementation: simulator/fw_engine.py, simulator/rasterizer.py.

7 s vs 234 s
point-scatterer cost at 200 against 40,000 scatterers per square metre; the Fourier method's cost does not move with density at all
docs/fw_calibration_tests.md
2.5e-7
maximum relative difference in the echo after the GPU rewrite of Steps A and B, which took the same scene from 1129 s to 101 s
docs/fw_calibration_tests.md

The argument

They agree, and they agree with the paper.

Two forward models sharing no mathematics and no code path should not agree by accident. Across a twelve-point range sweep, the Fourier-wavefield and point-scatterer backends land on the same peak pixel at every point, with peak levels within half a decibel and resolution within one percent.

Separately, the simulator reproduces Figure 3 of the Sanford 2024 paper with pass/fail gates in the test suite, so a regression that breaks the replication breaks the build rather than quietly degrading the imagery.

-0.42 dB
absolute peak-level agreement between the two backends, 0.20 dB spread over twelve range points, identical peak pixel at all twelve
docs/fw_calibration_tests.md
0.99 to 1.00
ratio of the two backends' 3 dB resolution in range; 0.998 to 1.005 along track
docs/fw_calibration_tests.md
25.0 / 12.5 mm
along-track and range 3 dB widths reproducing Sanford 2024 Fig. 3; the second backend lands within 10 % on both
run_sanford_fig3_tdbp.py

Both engines write the identical HDF5 layout, so nothing downstream knows or cares which one produced a file.

One engine, several instruments

The same scene file, imaged three ways.

The sonar is a parameter, not an assumption. Six named systems ship with geometry derived from their datasheets, and beamwidths, pulse repetition frequency, and element sizes are computed from physics rather than hardcoded. The same scene can be rendered as a synthetic-aperture collection, as a real-aperture side-scan line, or, changing only the band, as airborne Ka-band radar at 16.7 GHz through the same back-projection beamformer.

That makes cross-modality transfer a property of the scene file rather than a separate research project: the same ground truth, the same geometry, three sensors.

Six panels comparing the same cargo container, cylinder and boat hull imaged by real-aperture side-scan sonar and by synthetic aperture sonar
One scene through two imaging chains. Top: real-aperture side-scan, where along-track resolution spreads with range. Bottom: synthetic aperture, holding fine resolution across the whole swath.

Appendix

The point-scatterer observation model, in full.

This is the acoustics rather than the summary; the observation model, the wavefront solver, the amplitude terms, shadowing, speckle statistics, calibration, and the GPU kernel that evaluates it.

A PoSSM-style coherent point-scatterer simulator; every ping is a time-domain sum of per-scatterer echoes, each carrying its own delay, amplitude, and carrier phase.

1. Observation model

For ping \(k\) on channel \(c\), the complex baseband pressure is a coherent sum over scatterers \(i\):

\[ s_{k,c}(t) \;=\; \sum_{i} a_{k,c,i}(t)\;p\!\left(t - \tau_{k,c,i}(t)\right)\;\exp\!\left(-j\,2\pi f_c\,\tau_{k,c,i}(t)\right), \]

where \(p(\cdot)\) is the baseband LFM chirp, \(f_c\) is the carrier (300 kHz default), and

\[ \tau_{k,c,i}(t) \;=\; \frac{\lVert \mathbf{x}_i - \mathbf{x}^{TX}_k(t)\rVert \;+\; \lVert \mathbf{x}_i - \mathbf{x}^{RX}_{k,c}(t)\rVert}{c_s} \]

is the bistatic two-way time between the TX phase center and the \(c\)-th RX element. Production runs are continuous-motion: the RX position is evaluated at the echo arrival time, not at ping start. Stop-and-hop is disabled by default and is forbidden in production imaging.

2. Wavefront–receiver intersection (the τ solver)

The bistatic arrival time \(\tau\) is not a closed-form quantity, because the receiver moves while the echo is in flight. The true \(\tau\) satisfies the implicit equation

\[ \tau \;=\; \frac{d_{TX} \;+\; \bigl\lVert \mathbf{x}_i - \mathbf{x}^{RX}(\tau)\bigr\rVert}{c_s}, \qquad \mathbf{x}^{RX}(\tau) \;=\; \mathbf{x}^{RX}_0 + \mathbf{v}\,\tau, \]

i.e. \(\tau\) is the time at which the outgoing wavefront, after bouncing off the scatterer, intersects the receiver's straight-line trajectory. With the closed-form expansion of \(d_{RX}(\tau)^2\) along the trajectory,

\[ d_{RX}(\tau)^{2} \;=\; d_{RX}(0)^{2} \;-\; 2\,\tau\,\bigl(\mathbf{v}\!\cdot\!\Delta\mathbf{x}_{RX}\bigr) \;+\; \tau^{2}\,\lVert\mathbf{v}\rVert^{2}, \qquad \Delta\mathbf{x}_{RX} = \mathbf{x}_i - \mathbf{x}^{RX}_0, \]

the implicit equation becomes a Picard fixed-point iteration that we solve directly on the GPU:

\[ \tau^{(n+1)} \;=\; \frac{d_{TX} \;+\; \sqrt{\,d_{RX}(0)^{2} \;-\; 2\,\tau^{(n)}\,(\mathbf{v}\!\cdot\!\Delta\mathbf{x}_{RX}) \;+\; \tau^{(n)\,2}\,\lVert\mathbf{v}\rVert^{2}\,}}{c_s}, \qquad \tau^{(0)} \;=\; \frac{d_{TX}+d_{RX}(0)}{c_s}. \]

The initial guess \(\tau^{(0)}\) is exactly the stop-and-hop delay. Each iteration is a contraction with rate \(\sim\!\lVert\mathbf{v}\rVert/c_s\): about \(10^{-3}\) at SAS speeds: so one iteration drops the residual TOF error from \(O(v R / c_s^{2})\) (sub-cm at 100 m range) into the noise floor of the FP64 carrier phase. The default is one iteration (\(\mathtt{PS\_N\_TOF\_ITERS}=1\)), chosen not for speed but for an exact match to the beamformer's \(\mathtt{n\_tof\_iters}=1\) MoComp model: sim and BF use the same \(\tau\), so any residual phase error cancels coherently in TDBP rather than smearing the PSF.

The signature of getting this wrong is the asymmetric along-track sidelobe pattern of Cook & Brown (2009): the linear-phase-error fingerprint of a stop-and-hop sim driven through a continuous-motion beamformer. The solver above is what keeps it out of the imagery.

3. Per-scatterer amplitude

The complex amplitude \(a_{k,c,i}\) factorizes (linear pressure, not intensity):

\[ a_{k,c,i} \;=\; \rho_i \cdot \underbrace{B_{TX}(\hat{\mathbf{u}}^{TX}_i)\,B_{RX}(\hat{\mathbf{u}}^{RX}_{c,i})}_{\text{rectangular-piston sinc}}\;\cdot\;\underbrace{\cos\theta_i^{n}}_{\text{Lambert}}\;\cdot\;\underbrace{\frac{1}{d_{TX,i}\,d_{RX,c,i}}}_{\text{two-way spreading}}\;\cdot\;\underbrace{e^{-\alpha(f_c)\,(d_{TX,i}+d_{RX,c,i})}}_{\text{Thorp absorption}}\;\cdot\;V_i. \]

Each piston is a separable 2D sinc keyed on body-frame direction cosines, \(B(\hat{\mathbf{u}}) = |\mathrm{sinc}(\pi L_{az} u_x / \lambda)|\cdot|\mathrm{sinc}(\pi L_{el} u_z / \lambda)|\); attitude (R/P/Y) rotates the beam through \(R_{NED \to body}\). \(\cos\theta_i^{n} = |\hat{\mathbf{u}}^{TX}_i \!\cdot\! \hat{\mathbf{n}}_i|\) uses the heightfield surface normal at the scatterer: this is the Lambert directivity of Brown et al. 2019 (PoSSM). Per-scatterer reflectivities \(\rho_i = |\rho_i|\,e^{j\phi_i}\) carry Rayleigh-distributed magnitude with uniform phase. Absorption \(\alpha(f_c)\) is the Thorp seawater formula (~0.067 dB/m at 300 kHz).

4. Geometric shadows

The visibility factor \(V_i \in \{0,1\}\) (or a soft Fresnel knife-edge value in \([0,1]\) when enabled) comes from a DDA ray-march of the TX-to-scatterer segment against the heightfield: the ray is occluded at the first cell where \(z_{ray} > z_{terrain}\). Surface normals \(\hat{\mathbf{n}}_i\) are read from the same heightfield, clamped to a 60° max-slope so single-cell object edges cannot mute their surroundings.

5. Speckle and scatterer density

Scatterers are placed uniformly on the heightfield surface (with sub-wavelength normal-aligned jitter to avoid coherent halos). Per Goodman, fully-developed speckle requires \(N_{cell} \gg 1\) random complex scatterers per resolution cell; the bottom-type amplitude is calibrated by

\[ \sigma_z = \sqrt{\frac{\sigma_{bs}(20^\circ)}{2\,\rho\,\sin^2 20^\circ}}, \qquad \mathbb{E}[|\rho_i|] = \sigma_z\sqrt{\pi/2}, \]

where \(\rho\) is the scatterer density (m\(^{-2}\)) and \(\sigma_{bs}(20^\circ)\) is the APL-UW backscattering strength (e.g. \(-28\) dB for sand). The amplitude scaling absorbs \(\rho\) so the rendered intensity is density-invariant; \(\rho = 2000\)/m\(^2\) is the production working point, \(\rho = 200\)/m\(^2\) for debug.

6. Calibration constant

A deterministic calibration constant \(K_{PS}\) ties the simulator output to a closed-form analytic prediction for a known point target:

\[ |\text{peak}|^2_{\text{dB}} \;=\; TS \;-\; 40\log_{10}R \;-\; 2\alpha R \;+\; 10\log_{10}(BT), \]

which combines two-way spreading, Thorp absorption, and the LFM matched-filter coherent gain \(10\log_{10}(BT)\). The calibration harness drives a single target through the full pipeline and asserts agreement to within ±1 dB.

7. GPU kernel

One CUDA thread per \((c, i)\) pair computes the carrier rotation \(e^{-j2\pi f_c \tau}\) in FP64, multiplies the pre-sampled chirp, and atomicAdds the result into an upsampled \((n_{ch}, K\!\cdot\!n_{samp})\) accumulator. The buffer is then FFT-domain low-passed and decimated to \(f_s\): the oversampling preserves sub-sample delay precision that would otherwise quantize to \(\pm 1/(2 f_s)\) range bins.

Next: the labels that come with every image, or why a synthetic aperture exists at all.