Point target in front of a wall

Simulate a 77 GHz MIMO radar, a point target and a concrete wall, check the echo against the radar equation, and find the multipath peak.

In this tutorial you simulate the smallest complete scene: a 3-TX, 4-RX radar at the origin, a 1 m² point target 3 m in front of it, and a concrete wall at 5 m. You then check two things whose answer you know in advance: the echo strength against the free-space radar equation, and the position of the wall's multipath peak. It is a trimmed version of examples/single_point.py in the Radar repository.

You need a working installation and a CUDA GPU.

The script#

import math
 
import torch
from witwin.core import Mesh, PhysicalMaterial, Scene, Structure
from witwin.radar import Noise, PointTargets, Radar
from witwin.radar.policy import SPEED_OF_LIGHT_M_PER_S
 
CONFIG = {
    "num_tx": 3, "num_rx": 4, "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": 128, "power": 15,
    "tx_loc": [[0, 0, 0], [4, 0, 0], [2, 1, 0]],
    "rx_loc": [[-6, 0, 0], [-5, 0, 0], [-4, 0, 0], [-3, 0, 0]],
}
TARGET = (0.0, 0.0, -3.0)
RCS = 1.0
WALL_Z = -5.0
 
radar = Radar.from_dict(CONFIG, noise=Noise(figure=10.0), seed=20260727, look_at=(0.0, 0.0, -1.0))
 
wall = Mesh(
    vertices=torch.tensor([(-2.0, -2.0, WALL_Z), (2.0, -2.0, WALL_Z), (2.0, 2.0, WALL_Z), (-2.0, 2.0, WALL_Z)]),
    faces=torch.tensor([(0, 1, 2), (0, 2, 3)]),
    recenter=False, fill_mode="surface", topology_diagnostics=False,
)
scene = Scene(
    structures=(Structure(geometry=wall, material=PhysicalMaterial(name="concrete", eps_r=5.24, sigma_e=0.0462),
                          structure_id=1, material_id=1, assignment_id=1, surface_id=1),),
    endpoints=[],
)
targets = PointTargets(positions=[TARGET], rcs=RCS)
 
result = radar.simulate(scene, targets, times=(0.0, 0.1, 0.2), los=True, reflections=1)
print("cube", tuple(result.cube.shape))
print("compiles", result.compile_count, "discoveries", result.discovery_count, "epochs", result.epochs)
 
paths = result.last_radar_paths
print("rows", paths.path_count, "pairs", paths.sensor_pair_count)
 
# Free-space radar equation for the direct round trip.
wavelength = SPEED_OF_LIGHT_M_PER_S / radar.carrier
r = math.dist((0.0, 0.0, 0.0), TARGET)
spreading = wavelength / (4 * math.pi * r)
strength = math.sqrt(4 * math.pi * RCS) / wavelength
predicted = math.sqrt(radar.transmit_power_watts) * spreading * strength * spreading
measured = float(paths.complex_transfer_ref.abs().max())
print(f"|C| measured {measured:.6e}, radar equation {predicted:.6e}")
 
frame = result.frame(0)
rd = frame.range_doppler(window="hann")
combined = rd.data.reshape(-1, *rd.data.shape[-2:]).sum(dim=0)
response = combined.abs().amax(dim=0)
peak = int(response.argmax())
print(f"range peak {float(frame.axes.range_m[peak]):.4f} m (bin {frame.axes.range_bin_m:.4f} m)")
 
image_range = 0.5 * (r + math.dist((0.0, 0.0, 2 * WALL_Z), TARGET))
k = int(round(image_range / frame.axes.range_bin_m))
ratio = float(response[k - 2:k + 3].max() / response[peak])
print(f"multipath near {image_range:.3f} m, {20 * math.log10(ratio):.1f} dB below the direct peak")

Output on an RTX 5080:

cube (3, 3, 4, 128, 256)
compiles 1 discoveries 1 epochs (0, 0, 0)
rows 48 pairs 12
|C| measured 1.726919e-06, radar equation 1.726922e-06
range peak 3.0051 m (bin 0.0429 m)
multipath near 5.000 m, -10.1 dB below the direct peak

What the output shows#

  1. One compile, one discovery

    Nothing moves, so Radar compiles the Scene once and discovers the path set once for all three frames. Every frame belongs to epoch 0. See Simulation pipeline.

  2. 48 rows over 12 sensor pairs

    Each of the 12 TX-RX pairs has four round trips: out and back on the direct path, out direct and back via the wall, the reverse, and via the wall both ways. last_radar_paths holds the composed rows of the last frame.

  3. The radar equation

    The strongest row is the direct round trip. Its magnitude is the product of the transmit amplitude sqrt(P_tx), two free-space spreading factors lambda / (4 pi R) and the target strength sqrt(4 pi sigma) / lambda. The simulated value agrees to 2e-6 relative. complex_transfer_ref is the transfer coefficient at the carrier, before the waveform and the receiver chain.

  4. The range peak

    The coherent sum over the 12 pairs peaks at 3.005 m, within one 4.3 cm range bin of the true 3 m.

  5. The multipath peak

    The wall mirrors the target to z = -7 m. A round trip that goes direct one way and via the wall the other way has the mean of the two lengths, 5 m. Both cross terms land there, about 10 dB below the direct peak. The wall-both-ways path is at 7 m.

Things to try#

  • Set reflections=0 and the 5 m peak disappears, leaving 12 rows.
  • Change eps_r of the wall. A larger permittivity raises the multipath peak.
  • Remove noise=... from Radar.from_dict. The cube is then noiseless, and CFAR in frame.points() detects sidelobes rather than only targets. See Detect targets and build point clouds.