Use OFDM and pulsed waveforms
Build OFDM and pulsed (rectangular or LFM) radars, check their constraints, and process their cubes into range profiles and range-Doppler maps.
FMCW is the default for mmWave sensing, but the same Scene and targets can be simulated with an OFDM or a pulsed waveform. Build the Radar directly with an Ofdm or Pulsed record; the flat configuration format is FMCW only.
OFDM#
from witwin.radar import Ofdm, PointTargets, Radar
ofdm = Ofdm(
subcarrier_spacing=120e3, # Hz
num_subcarriers=256,
cyclic_prefix=0.6e-6, # s
num_symbols=32,
max_expected_delay=0.1e-6, # s, longest round trip you configure for
)
radar = Radar(carrier=28e9, waveform=ofdm, tx=[(0.0, 0.0, 0.0)], rx=[(0.0, 0.0, 0.0)], power=20.0)
result = radar.simulate(scene, PointTargets(positions=[(0.0, 0.0, -3.0)], rcs=1.0), times=(0.0,), reflections=0)
print(result.axis_names) # ('frame', 'tx', 'rx', 'symbol', 'subcarrier')The cube is the channel frequency response per symbol and subcarrier, as per-subcarrier equalization would leave it. It is not a time-domain waveform.
Constraints, checked at construction or synthesis:
max_expected_delaymust be shorter thancyclic_prefix. Outside the cyclic prefix the single-tap model no longer holds, so Radar refuses instead of clamping.- The range resolution is
c / (2 · num_subcarriers · subcarrier_spacing): 4.9 m for the example above. Use more bandwidth for finer range. - The maximum unambiguous delay is
1 / subcarrier_spacing. - Synthesis is narrowband: material and antenna response are frozen at the carrier across the band.
subcarrier_origin is "f_ref_at_n0", the only supported value: subcarrier n sits at carrier + n · subcarrier_spacing.
Pulsed#
from witwin.radar import Pulsed, Radar
pulsed = Pulsed(
pulse_kind="lfm", # "rect" or "lfm"
pulse_width=1e-6, # s
bandwidth=150e6, # Hz, the LFM sweep
pri=50e-6, # s, pulse repetition interval
num_pulses=16,
sample_rate=300e6, # Hz
num_samples=512,
range_gate_start=0.0, # s, delay of the first sample
)
radar = Radar(carrier=10e9, waveform=pulsed, tx=[(0.0, 0.0, 0.0)], rx=[(0.0, 0.0, 0.0)], power=30.0)The cube axes are ('frame', 'tx', 'rx', 'pulse', 'sample'). The cube is the received baseband pulse train inside the range gate, the input to a matched filter.
Constraints:
- The pulse must be shorter than
pri, and the range gate (range_gate_start + num_samples / sample_rate) must fit inside onepri. - For
pulse_kind="rect", setbandwidthto1 / pulse_width, the rectangle's own matched-filter bandwidth. max_expected_delay_rate(default0.0) bounds|d(tau)/dt|. Over the coherent processing intervalnum_pulses · pri, the delay must not walk more than one range cell,1 / bandwidth. Set it when targets move fast.pulse_normalizationis"unit_energy", the only supported value.
Range resolution is c / (2 · bandwidth) for LFM and c · pulse_width / 2 for a rectangle. The maximum unambiguous range is c · pri / 2.
Processing#
The Frame methods dispatch on the waveform, so the code is the same as for FMCW:
frame = result.frame(0)
profile = frame.range_profile() # OFDM: inverse DFT over subcarriers. Pulsed: matched filter
rd = frame.range_doppler() # Doppler FFT over symbols or pulses
peak = rd.data.abs().sum(dim=(0, 1)).amax(dim=0).argmax()
print(float(rd.range_axis[peak]))For the pulsed waveform, the matched filter correlates against the analytic pulse replica. witwin.radar.processing.matched_filter, lag_axis and pulse_samples expose the pieces if you want to run the filter yourself, for example with oversample=4 to interpolate the lag grid.
Limits#
- Leakage (
Radar.leakage) is FMCW only. - Oscillator phase noise in a scene simulation is FMCW only.
Motion.adaptive()is FMCW only. OFDM refreshes the world once per symbol and pulsed once per pulse underMotion.chirp().