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 loss

Create 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 cube

The 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#

ErrorCause
Radar.position carries a gradientThe pose is not differentiable. Move antennas through SensorEndpointIds instead
A configuration value is refused as a tensorWaveform and receiver settings must be Python floats
ValueError about a per-target strength vectorGive one scalar rcs, or add an Aspect and give the vector as amplitude
RuntimeError on create_graph=TrueOnly first-order derivatives are supported