Angle estimation
ArrayGeometry, beamforming with conventional and MVDR weights, TDM phase compensation, FFT angle estimators and MUSIC in witwin.radar.processing.
This page covers the angle stage of witwin.radar.processing. All directions are in the array's local frame.
from witwin.radar.processing import (ArrayGeometry, BeamCube, beam_cube, conventional_steering, fft2_aoa,
music_image, music_spectrum, mvdr_weights, phase_comparison_aoa,
tdm_compensate)The virtual array is ordered TX-major: pair p belongs to transmitter p // num_rx and receiver p % num_rx. A TDM-MIMO virtual element sits at the sum of its transmitter and receiver offsets. The offsets are the tx and rx positions of the radar, in its local frame: x to the right, y up, boresight along -z. For a planar array in the x-y plane, only the x and y parts of a direction change the steering phase.
ArrayGeometry#
class ArrayGeometry(element_positions_m, num_tx, num_rx, element_spacing_m, wavelength_m, phase_sign,
tx_loc_half_wavelength, rx_loc_half_wavelength)Positions of every virtual element and the sign of its phase. Get one with frame.array().
| Field or property | Meaning |
|---|---|
element_positions_m | [P, 3] float64 virtual element positions, m, local frame, TX-major. |
num_tx, num_rx | Array size. sensor_pair_count is their product. |
element_spacing_m | One unit of the element grid, m. |
wavelength_m | Carrier wavelength, m. |
phase_sign | Sign of the propagation phase, from the cube's phasor. |
tx_loc_half_wavelength, rx_loc_half_wavelength | Element offsets in grid units. |
spacing_wavelengths | Grid unit in wavelengths, 0.5 for a half-wavelength array. |
transmitter_index, receiver_index | [P] int64 index of each pair's transmitter and receiver. |
device | Device of element_positions_m. |
ArrayGeometry.from_axes#
ArrayGeometry.from_axes(axes) -> ArrayGeometryBuild from a ProcessingAxes. frame.array() calls this.
ArrayGeometry.from_offsets#
ArrayGeometry.from_offsets(tx_loc, rx_loc, *, element_spacing_m, wavelength_m, phase_sign=-1, device='cpu') -> ArrayGeometryBuild from two element-offset grids in units of element_spacing_m. Use it for an array that is not half-wavelength spaced.
conventional_steering#
conventional_steering(array, directions, *, normalize=True, dtype=torch.complex64) -> torch.TensorPhase-shift weights [P, *beam] for a grid of look directions.
The virtual array.
[*beam, 3] unit vectors from the array toward each look direction, local frame. Not normalized for you.
TrueScale the weights so w^H a = 1 for a matched wavefront. A beam cube then has the same amplitude convention as the range and Doppler stages.
torch.complex64Weight dtype.
mvdr_weights#
mvdr_weights(covariance, steering, *, diagonal_loading) -> torch.TensorMinimum-variance distortionless-response weights w = R^-1 a / (a^H R^-1 a), [..., P, B]. They satisfy the same w^H a = 1 constraint as conventional_steering, so the two are interchangeable in beam_cube.
Hermitian [..., P, P] covariance.
[P, *beam], as conventional_steering returns it.
Loading as a fraction of trace(R) / P, added to the diagonal before the solve. No default: a sample covariance from fewer snapshots than elements is singular, and only you know the snapshot count.
beam_cube#
beam_cube(rd, steering, *, directions) -> BeamCubeApply beamforming weights to a range-Doppler map: y[b] = sum_p conj(w[p, b]) · x[p].
Map with the sensor pairs in the leading axes, [TX, RX, D, R] or [P, D, R].
[P, *beam] weights.
[*beam, 3] grid the weights were steered over. Keyword only.
Returns BeamCube with data [*beam, D, R], axes, directions, and the range_axis and doppler_axis properties.
import torch
az = torch.linspace(-0.8, 0.8, 61, device=radar.device)
directions = torch.stack((torch.sin(az), torch.zeros_like(az), -torch.cos(az)), dim=-1) # local -z is boresight
weights = conventional_steering(frame.array(), directions)
beams = beam_cube(frame.range_doppler(), weights, directions=directions)
print(beams.data.shape) # [61, doppler, range]tdm_compensate#
tdm_compensate(aoa_input, velocities, array, axes) -> torch.TensorRemove the phase a moving target writes across TDM transmitters, which are sampled one chirp apart. Apply it before an angle estimate on a moving target.
[P, N] complex virtual-array snapshots for N cells, TX-major.
[N] radial velocity of each cell, m/s, closing positive.
The virtual array.
The cube's axes. The raw chirp period is axes.slow_time_period_s / num_tx.
Returns a tensor of the same shape and dtype.
phase_comparison_aoa#
phase_comparison_aoa(virtual_ant, array, *, fft_size=64) -> torch.TensorDirection cosines [3, N] from two one-dimensional FFTs: azimuth from the first 2 · num_rx elements, elevation from the phase difference to the sub-aperture 2 · num_rx elements later. On a half-wavelength array, x = sin(azimuth). Elevation is coarse: the correction uses the azimuth peak bin, not the continuous azimuth.
[P, N] complex snapshots, TX-major.
The virtual array. Needs an elevation sub-aperture.
64FFT size of each angle transform.
fft2_aoa#
fft2_aoa(virtual_ant, array, *, fft_size=64) -> torch.TensorDirection cosines [3, N] from one zero-padded two-dimensional FFT over a planar virtual array of num_tx // 2 by 2 · num_rx elements. Reads both angles off one grid, so elevation is more accurate than with phase_comparison_aoa.
Raises ValueError for an array with fewer than four transmitters.
music_spectrum#
music_spectrum(angle_data, array, *, elevation_rad, azimuth_rad, num_signals=7, spatial_smooth=3) -> torch.TensorMUSIC pseudo-spectrum [B, len(elevation_rad), len(azimuth_rad)] from [B, M, N, T] data: B range bins, an M by N planar array, T snapshots. Forward spatial smoothing averages the covariance over sub-arrays to decorrelate coherent multipath. Differentiable.
Complex [B, M, N, T].
The virtual array. Its spacing_wavelengths sets the steering phase.
Elevation scan grid, rad.
Azimuth scan grid, rad.
7Size of the signal subspace.
3Smoothing window, elements.
music_image#
music_image(profile, array, *, elevation_rad, azimuth_rad, range_bins, num_signals=7, spatial_smooth=3,
num_snapshots=8) -> torch.TensorA MUSIC image [elevation, azimuth, bins] from a range profile. Differentiable.
The range profile, so the image uses the same range grid as the rest of the pipeline.
The virtual array.
Elevation scan grid, rad.
Azimuth scan grid, rad.
Range bins to image. Required: picking a peak automatically would be a silent modelling choice.
7Size of the signal subspace.
3Smoothing window, elements.
8Slow-time snapshots used for the covariance.
See Resolve two targets with MUSIC for a complete example.
beam_cube, conventional_steering, mvdr_weights, tdm_compensate, music_spectrum and music_image are differentiable. phase_comparison_aoa and fft2_aoa pick peaks and refuse inputs that carry a gradient.