Reuse a trace with echo

Trace the world once with Radar.trace, then synthesize it with several receivers, seeds or FMCW output domains using Radar.echo.

Propagation is usually the expensive part of a simulation. If you want the same world seen through several receiver configurations, trace it once and echo it many times. radar.echo(radar.trace(...)) is bit-identical to radar.simulate(...).

Trace once, echo many times#

from dataclasses import replace
from witwin.radar import Noise
 
paths = radar.trace(scene, targets, times=(0.0, 0.1), los=True, reflections=1)
print(paths.kind, paths.frame_count, paths.observation_count, paths.row_count)
 
clean = radar.echo(paths)
noisy = [radar.replace(noise=Noise(figure=10.0), seed=s).echo(paths) for s in range(8)]
beat = radar.replace(waveform=replace(radar.waveform, output="beat")).echo(paths)

trace takes the same arguments as simulate and returns a Paths. echo takes only the Paths.

The echoing radar does not have to be the one that traced. What may differ:

  • The receiver chain: noise, lna_gain, iq, agc, adc, leakage, impedance.
  • seed.
  • The FMCW output domain (Fmcw.output).
  • For a static trace (paths.kind == "static"), the waveform as well, because the world was evaluated once per frame.

What must match, or echo raises ValueError:

  • carrier, because the paths were composed at that reference frequency.
  • num_tx and num_rx, because the sensor-pair partition is frozen into the rows.
  • The waveform, for a dynamic trace (chirp, adc or adaptive), because the world was evaluated at that waveform's instants.
  • A receiver with oscillator phase noise needs a trace taken with Motion.adc() or Motion.adaptive().

The beat example uses dataclasses.replace because waveform records are frozen dataclasses.

Inspect the rows#

Paths is also a way to look at what the world half produced:

frame = paths.frame(0)            # a Paths for one frame, still echo-able
rows = paths.rows(frame=0)        # the composed rows of the last observation of frame 0
print(rows.path_count, rows.sensor_pair_count)
print(rows.total_delay_s[:4], rows.complex_transfer_ref[:4].abs())

rows(frame, observation=-1) returns one observation's rows. An adaptive frame keeps its rows in an interpolation table, so only observation=0 or -1 is available there; anything else raises IndexError.

Memory#

A Paths keeps every evaluated observation's rows, about 20 bytes per live row per observation. A static frame has one observation. A Motion.adc() FMCW frame has chirps · num_tx · samples observations, which for a 3-TX, 128-chirp, 256-sample frame is 98,304. If you trace a long dynamic sequence, trace and echo one frame at a time:

for t in times:
    result = radar.echo(radar.trace(scene, targets, times=(t,)))
    consume(result)

simulate and stream never hold more than one observation's rows at a time, so prefer them unless you echo more than once.