Moving target and Doppler
Give a point target a trajectory, simulate four frames with chirp-rate motion sampling, and read range and radial velocity from the point cloud.
In this tutorial you simulate a person-sized point target walking toward the radar at 1.5 m/s next to a target that stands still. You read the walker's range and radial velocity from four frames, see how the velocity resolution follows from the waveform, and compare chirp-rate motion sampling with the exact ADC-rate reference.
Read Motion sampling for the background on the motion modes.
The script#
import torch
from witwin.core import Scene
from witwin.radar import Motion, Noise, PointTargets, Radar
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=7, look_at=(0.0, 0.0, -1.0))
scene = Scene(structures=(), endpoints=[])
SPEED = 1.5 # m/s toward the radar
START = torch.tensor([[0.0, 0.0, -4.0], [0.6, 0.0, -2.5]], device=radar.device)
def trajectory(t):
# Target 0 walks toward the radar along +z; target 1 stands still.
positions = START.clone()
positions[0, 2] += SPEED * float(t)
return positions
targets = PointTargets(positions=START, rcs=1.0, trajectory=trajectory)
times = (0.0, 0.1, 0.2, 0.3)
result = radar.simulate(scene, targets, times=times, reflections=0, motion=Motion.chirp())
print("motion", result.motion_sampling, "cube", tuple(result.cube.shape))
axes = result.frame(0).axes
print(f"velocity bin {axes.velocity_bin_mps:.4f} m/s, max unambiguous speed {axes.max_unambiguous_speed_mps:.3f} m/s")
for k in range(result.frame_count):
cloud = result.frame(k).points(max_points=8)
strongest = cloud.energy.argsort(descending=True)[:2]
found = sorted((round(float(cloud.range_m[i]), 3), round(float(cloud.velocity_mps[i]), 3)) for i in strongest)
print(f"t={result.times_s[k]:.1f} s (range m, velocity m/s): {found}")
try:
radar.simulate(scene, targets, times=times, reflections=0, motion=Motion.static())
except ValueError as error:
print("Motion.static refused:", str(error)[:120])Output on an RTX 5080:
motion chirp cube (4, 3, 4, 128, 256)
velocity bin 0.0704 m/s, max unambiguous speed 4.506 m/s
t=0.0 s (range m, velocity m/s): [(2.576, 0.0), (3.993, 1.549)]
t=0.1 s (range m, velocity m/s): [(2.576, 0.0), (3.821, 1.549)]
t=0.2 s (range m, velocity m/s): [(2.576, 0.0), (3.692, 1.549)]
t=0.3 s (range m, velocity m/s): [(3.52, 1.479), (3.52, 1.549)]
Motion.static refused: Motion.static() asks for one observation per frame, but this session moves within a frame; use Motion.adc() for the exhaWhat the output shows#
A trajectory makes the world move
trajectory(t)returns both positions at timet. Radar calls it at every observation instant, so the walker moves inside each frame as well as between frames.Motion.static()would freeze each frame and is refused for a world that moves.Velocity resolution
With 3 transmitters in TDM, one transmitter repeats every
3 · 72 µs = 216 µs. Over 128 chirps that gives a velocity bin ofλ / (2 · 128 · 216 µs) = 0.0704 m/sand an unambiguous speed of ±4.5 m/s. The walker's 1.5 m/s lands on the nearest bin, 22 bins or 1.549 m/s. Positive velocity means closing.Range follows the walk
The walker starts at 4 m and closes 0.15 m per frame. The detected range moves 3.993, 3.821, 3.692, 3.52 m, within one 4.3 cm range bin of the true 4.00, 3.85, 3.70 and 3.55 m. The standing target stays at 2.576 m, the bin nearest its true 2.571 m, with zero velocity.
Two strongest cells
The script prints the two strongest points of each frame. At
t = 0.3 sboth belong to the walker, in two neighbouring Doppler bins, so the standing target is not among the two. Raise the[:2]slice to see it.
Chirp-rate against ADC-rate sampling#
Motion.chirp() freezes the geometry within each chirp and moves it between chirps (stop and hop). Motion.adc() moves it at every ADC sample, which is exact but much slower. To compare them on one frame, append:
import time
for motion in (Motion.chirp(), Motion.adc()):
begin = time.perf_counter()
cube = radar.simulate(scene, targets, times=(0.0,), reflections=0, motion=motion).cube
torch.cuda.synchronize()
print(f"{motion.kind}: {time.perf_counter() - begin:.2f} s")
if motion.kind == "chirp":
reference = cube
error = (cube - reference).norm() / cube.norm()
print(f"relative difference chirp vs adc: {float(error):.2e}")Output on an RTX 5080:
chirp: 4.41 s
adc: 1085.52 s
relative difference chirp vs adc: 7.39e-02ADC-rate sampling is slow
This frame has 128 · 3 · 256 = 98304 ADC instants, and Motion.adc() evaluates the world at every one of them. It took 18 minutes here. Use Motion.chirp() for exploration and Motion.adaptive() when you need ADC-rate accuracy on long sequences.
The 7% difference is the motion within each chirp that stop-and-hop leaves out. It matters most for losses on the raw cube and for fast targets.
Things to try#
- Give the walker a sideways component and watch the point cloud's
xmove while the radial velocity drops. - Raise
SPEEDabove 4.5 m/s and watch the velocity alias to the negative side. - Replace
Motion.chirp()withMotion.adaptive()and readresult.adaptive_diagnostics.