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_txandnum_rx, because the sensor-pair partition is frozen into the rows.- The waveform, for a dynamic trace (
chirp,adcoradaptive), because the world was evaluated at that waveform's instants. - A receiver with oscillator phase noise needs a trace taken with
Motion.adc()orMotion.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.