Quickstart

Simulate a 3-TX, 4-RX 77 GHz FMCW radar looking at a point target in front of a concrete wall, then read range and a point cloud.

This page runs one complete simulation in a few seconds: a 77 GHz TDM-MIMO FMCW radar with 3 transmit and 4 receive antennas looks at a point target 3 m away, in front of a concrete wall at 5 m. You read the target's range from a range-Doppler map and get a point cloud. You need a working installation.

The script#

Save this as quickstart.py and run python quickstart.py.

import torch
from witwin.core import Mesh, PhysicalMaterial, Scene, Structure
from witwin.radar import Noise, PointTargets, Radar
 
# A TI-style FMCW configuration. Vendor units: slope in MHz/us, sample rate in ksps,
# times in us, power in dBm, antenna positions in half wavelengths.
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]],
}
radar = Radar.from_dict(CONFIG, noise=Noise(figure=10.0), seed=1, look_at=(0.0, 0.0, -1.0))
 
# A 4 m x 4 m concrete wall at z = -5 m. recenter=False keeps the world coordinates.
wall = Mesh(
    vertices=torch.tensor([(-2.0, -2.0, -5.0), (2.0, -2.0, -5.0), (2.0, 2.0, -5.0), (-2.0, 2.0, -5.0)]),
    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=[],
)
 
# One point target with a 1 m^2 radar cross section, 3 m in front of the radar.
targets = PointTargets(positions=[(0.0, 0.0, -3.0)], rcs=1.0)
 
# Two frames, 0.1 s apart. Line of sight plus one bounce off the wall.
result = radar.simulate(scene, targets, times=(0.0, 0.1), los=True, reflections=1)
print(tuple(result.cube.shape), result.axis_names)
 
frame = result.frame(0)
rd = frame.range_doppler()
power = rd.data.abs().sum(dim=(0, 1))
peak_range = rd.range_axis[power.amax(dim=0).argmax()]
print(f"range bin {frame.axes.range_bin_m:.4f} m, peak at {float(peak_range):.3f} m")
 
cloud = frame.points(max_points=64)
print(f"{len(cloud)} points, first xyz: {cloud.xyz[:1].tolist()}")

Output on an RTX 5080:

(2, 3, 4, 128, 256) ('frame', 'tx', 'rx', 'chirp', 'range_bin')
range bin 0.0429 m, peak at 3.005 m
40 points, first xyz: [[-0.0, 2.9189955462294943, 0.040015167080793015]]

The first call loads the CUDA library and compiles the scene for Channel, so it takes a few seconds longer than later calls.

What happened#

  1. Build the radar

    Radar.from_dict reads the flat FMCW configuration format, which uses the units a TI radar configuration file quotes, and converts them to SI. Keyword arguments after the mapping set the other Radar fields in SI: here a thermal noise stage with a 10 dB noise figure, a random seed, and the pose. The radar sits at the origin and looks along -z. See Radar and waveforms.

  2. Build the world

    The Scene is a witwin.core Scene with one structure: a two-triangle wall with a concrete material. Radar does not place scatterers on meshes. Meshes reflect, and you declare the scatterers as targets. See Scenes and targets.

  3. Simulate

    radar.simulate traces the line-of-sight and one-bounce paths between the radar, the target and the wall, synthesizes the FMCW echo for every chirp, adds receiver noise, and stacks the frames. The cube has the axes ('frame', 'tx', 'rx', 'chirp', 'range_bin'): FMCW output defaults to a range spectrum, so the last axis is already range.

  4. Process

    result.frame(0) returns one frame with its axes. range_doppler() applies the Doppler FFT with a Hann window. points() runs range-Doppler, CA-CFAR detection and angle estimation and returns a point cloud in the array frame. See Signal processing chain.

Read the point cloud#

Point cloud coordinates are in the radar array's local frame, not the world frame: x is the array's horizontal axis, y is the distance along the boresight, and z is elevation. So the target at world (0, 0, -3) appears near (0, 2.92, 0.04).

Each point is one detected range-Doppler cell, so one target and the wall produce several points. Every row also carries velocity_mps, energy (dB) and range_m; cloud.as_columns() returns all six columns as one tensor.

Next steps#