Resolve two targets with MUSIC

Build a 20 x 20 virtual planar array, gate the range profile, and separate two targets 19 degrees apart with a MUSIC angle image.

In this tutorial you separate two targets at the same range with MUSIC, a subspace method that resolves angles more finely than an FFT over the array. You build a 20-TX by 20-RX radar whose virtual array is a planar 20 x 20 grid, gate the range profile around the targets, and read the two angles off the image. It is a trimmed version of examples/music_imaging.py in the Radar repository.

The script#

import math
 
import torch
from witwin.core import Scene
from witwin.radar import Noise, PointTargets, Radar
from witwin.radar.processing import music_image
 
N = 20
CONFIG = {
    "num_tx": N, "num_rx": N, "fc": 77e9, "slope": 60.012, "adc_samples": 256,
    "adc_start_time": 6, "sample_rate": 4400, "idle_time": 7, "ramp_end_time": 65,
    "chirp_per_frame": 8, "power": 15,
    "tx_loc": [[i, 0, 0] for i in range(N)],        # transmit row along local x
    "rx_loc": [[N, -i, 0] for i in range(N)],       # receive row along local y
}
radar = Radar.from_dict(CONFIG, noise=Noise(figure=10.0), seed=20260727, look_at=(0.0, 0.0, -1.0))
targets = PointTargets(positions=[(-0.5, 0.0, -3.0), (0.5, 0.0, -3.0)], rcs=1.0)
 
result = radar.simulate(Scene(structures=(), endpoints=[]), targets, times=(0.0,), reflections=0)
print("cube", tuple(result.cube.shape), "rows", result.last_radar_paths.path_count)
 
frame = result.frame(0)
profile = frame.range_profile()
energy = profile.data.abs().sum(dim=(0, 1, 2))
peak = int(energy.argmax())
range_bins = torch.arange(peak - 2, peak + 3, device=radar.device)
print(f"range gate bins {peak - 2}..{peak + 2}, centre {float(frame.axes.range_m[peak]):.4f} m")
 
grid = torch.linspace(-math.pi / 4, math.pi / 4, 128, device=radar.device)
image = music_image(profile, frame.array(), elevation_rad=grid, azimuth_rad=grid,
                    range_bins=range_bins, num_signals=7, spatial_smooth=3, num_snapshots=8)
print("image", tuple(image.shape))
 
lobe = image.abs().amax(dim=2).amax(dim=1).cpu()
inner = lobe[1:-1]
peaks = torch.nonzero((inner > lobe[:-2]) & (inner > lobe[2:])).reshape(-1) + 1
top = peaks[lobe[peaks].argsort(descending=True)[:2]]
found = sorted(round(math.degrees(float(grid[i])), 2) for i in top)
expected = sorted(round(math.degrees(math.atan2(x, 3.0)), 2) for x in (-0.5, 0.5))
print("found deg", found, "expected deg", expected)

Output on an RTX 5080:

cube (1, 20, 20, 8, 256) rows 800
range gate bins 69..73, centre 3.0481 m
image (128, 128, 5)
found deg [-9.57, 10.28] expected deg [-9.46, 9.46]

The image grid has a step of 0.71°, so both lobes are within two pixels of the true angles.

How it works#

  1. A planar virtual array

    In TDM-MIMO, each TX-RX pair acts as one virtual element at the sum of the two offsets. Transmitters along local x and receivers along local y give a 20 x 20 grid of 400 virtual elements and 800 composed rows for two targets. frame.array() returns that geometry as an ArrayGeometry.

  2. A range gate

    music_image needs range_bins: it does not pick a peak for you, because that choice changes the answer. Here the gate is the five bins around the strongest range.

  3. Spatial smoothing

    The two echoes are coherent, which makes the sample covariance rank-deficient. spatial_smooth=3 averages the covariance over shifted sub-arrays to decorrelate them. num_signals is the assumed signal-subspace size.

  4. Read the angles

    The image is [elevation, azimuth, bins]. Both targets lie on the first axis, which follows the transmit row, so the script takes the maximum over the other two axes and finds the two strongest local maxima.

music_image and music_spectrum are differentiable. Reading a peak position off the image is not. See Differentiability.

Things to try#

  • Move the targets closer together and find the separation at which the two lobes merge.
  • Raise chirp_per_frame to get more snapshots for the covariance.
  • Lower num_signals to 2, the true number of sources here, and compare the image.