Take gradients
Differentiate a Radar data cube with respect to target positions, radar cross section, mesh vertices and material permittivity, in reverse or forward mode.
This page shows how to get first-order derivatives out of a simulation. Read Differentiability for the full list of supported and refused inputs.
Reverse mode: target position#
Mark the position tensor, pass grad="vjp", and call backward() on a scalar loss:
import torch
from witwin.radar import PointTargets
positions = torch.tensor([[0.0, 0.0, -3.0]], device=radar.device, requires_grad=True)
targets = PointTargets(positions=positions, rcs=1.0)
result = radar.simulate(scene, targets, times=(0.0,), reflections=0, grad="vjp")
loss = result.cube.abs().square().sum()
loss.backward()
print(positions.grad) # [1, 3], d(loss)/d(position) in 1/m units of the lossCreate the tensor on radar.device. PointTargets passes a tensor through untouched, so its tape reaches both propagation legs.
Reverse mode: radar cross section#
rcs (or amplitude) can be a 0-dim tensor:
rcs = torch.tensor(1.0, device=radar.device, requires_grad=True)
result = radar.simulate(scene, PointTargets(positions=[(0.0, 0.0, -3.0)], rcs=rcs),
times=(0.0,), reflections=0, grad="vjp")
result.cube.abs().square().sum().backward()
print(rcs.grad)Mark amplitude instead of rcs if your optimizer should work on the field strength directly; the rcs route puts a square root into the gradient.
Forward mode#
Forward mode gives the directional derivative of the whole cube along one tangent, in one pass:
import torch.autograd.forward_ad as forward_ad
base = torch.tensor([[0.0, 0.0, -3.0]], device=radar.device)
direction = torch.tensor([[0.0, 0.0, 1.0]], device=radar.device)
with forward_ad.dual_level():
dual = forward_ad.make_dual(base, direction)
result = radar.simulate(scene, PointTargets(positions=dual, rcs=1.0), times=(0.0,), reflections=0, grad="jvp")
primal, tangent = forward_ad.unpack_dual(result.cube)
print(tangent.shape) # same shape as the cubeThe primal is bitwise identical to a grad="none" run.
Scene parameters#
Mesh vertices and material permittivity are leaves too. Mark them before you build the Scene:
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)],
requires_grad=True)
eps_r = torch.tensor(5.24, requires_grad=True)
wall = Mesh(vertices=vertices, 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=eps_r, sigma_e=0.0462),
structure_id=1, material_id=1, assignment_id=1, surface_id=1),), endpoints=[])
result = radar.simulate(scene, targets, times=(0.0,), reflections=1, grad="vjp")
result.cube.abs().square().sum().backward()
print(vertices.grad, eps_r.grad)Only reflected paths depend on the wall, so the gradient comes from the multipath returns. The in-plane component of a vertex gradient is exactly zero for a flat reflector, which is correct.
Losses after processing#
The differentiable processing stages keep the tape: range_profile, range_doppler_map, beam_cube, matched_filter, tdm_compensate, music_spectrum and music_image.
rd = result.frame(0).range_doppler()
target_power = rd.data.abs().square().sum(dim=(0, 1)) # [doppler, range]
loss = -target_power[:, 60:80].sum()CFAR, point_cloud, the angle estimators and tracking refuse a tensor that carries a gradient. Detach explicitly if you only want to look at their output.
Noise in the loop#
With a receiver chain, noise is drawn from Radar.seed. The same seed gives the same noise and the same gradient on every call, so a fixed seed gives a deterministic objective. Use radar.replace(seed=step) to resample the noise each step.
Common errors#
| Error | Cause |
|---|---|
Radar.position carries a gradient | The pose is not differentiable. Move antennas through SensorEndpointIds instead |
| A configuration value is refused as a tensor | Waveform and receiver settings must be Python floats |
ValueError about a per-target strength vector | Give one scalar rcs, or add an Aspect and give the vector as amplitude |
RuntimeError on create_graph=True | Only first-order derivatives are supported |