Generate a coupled radar dataset¶
Generate a small dataset of coupled active-radar responses through the
stock CLI: ray-cast target visibility in a Sionna scene, closed-form
point-target returns composed through the rfgen signal chain. One configuration directory defines one dataset; rfgen validate,
rfgen generate, and rfgen inspect are the whole workflow.
Before starting, install the ray-tracing backend:
uv pip install -e '.[sionna]'
The default sionna_rt backend uses ray tracing for geometry only: a
deterministic ray cast decides whether each point target is visible or
shadowed by another. The returns themselves are closed-form (exact
radar-equation amplitude for the configured RCS, exact delay and
Doppler) composed through the rfgen signal chain, so no vendor license
or artifact is needed for generation.
The radarsimpy backend remains available as a validation-only oracle:
selecting it requires validation_study: true in the configuration plus
an authorized, platform-matched RadarSimPy artifact (see the RadarSimPy
note in Install).
Only the FMCW (frequency-modulated continuous-wave) CPU point-target scope
is runtime-qualified today; PMCW, pulsed-LFM, interference, and array
requests are rejected at simulate until each is runtime-qualified. The
stronger runtime acceptance gate, the bar for dataset claims, has not yet
passed for any scope; the
Radar Response reference is the
authority for the current status. rfgen validate resolves the radar
source and checks the unit, timing, and waveform-kind rules without
importing the ray-tracing engine. Capability gating, the sample-count and
max_output_bytes pre-checks, and a missing engine
(BackendUnavailableError, install the sionna extra) surface only at
rfgen generate.
Materialize and edit the configuration¶
rfgen init radar-response my-radar-dataset
The template is in two halves, and which half you edit depends on what you are changing.
plan: owns the scene: the clock, the radar system’s pose and aperture,
and plan.targets, whose bounds drive the per-sample point-target draws —
count, position, velocity, and radar cross section. This is the half you edit
to shape a dataset.
projections: names one projection, rfgen.radar.receiver, whose
params.renderer block holds the renderer-private values: the FMCW
waveform sweep, receiver sampling and noise figure, gains, backend_params,
and the optional frontend block (iq_amplitude_imbalance_db,
iq_phase_imbalance_rad, adc_enob_bits, adc_full_scale). The full surface
is enumerated in the
Radar Response reference.
The split is enforced, not conventional: a plan-owned fact written under
params.renderer is refused by name rather than silently overridden. Target
bounds, system pose, element locations, and pulse-repetition intervals are not
fields of RadarRendererParams at all.
Target draws, the baseband return, and the noise component
are all deterministic in the sample seed: the signal chain owns the
noise draw, so full records and their content IDs reproduce bit for bit
across re-runs. (Under the validation-only radarsimpy oracle, noise is
drawn by the vendor artifact outside seed control, so content IDs are
not reproducible there; that is one reason it is not a generation
backend.)
When you edit the bounds, keep them inside the waveform’s design envelope.
In the formulas below, slope is the swept bandwidth divided by the sweep
time, fs is the receiver sample_rate_hz, λ is the carrier wavelength
(c ÷ carrier_hz), PRI is the pulse repetition interval, and N is the
pulse count. For the shipped template the maximum representable range
c·fs/(4·slope) comes to about 120 m; a target whose beat would exceed
half the complex sampling rate is never folded to a false range.
Instead its return is dropped from the cube and the drop is recorded in
the record’s provenance (targets[].dropped_paths), exactly as an
occluded target is recorded under targets[].visible. After changing
bounds, check those fields on a sample record before scaling up. The unambiguous velocity λ/(4·PRI) is about 19.5 m/s, and
the velocity resolution λ/(2·N·PRI) is about 9.7 m/s at N=4. Velocity
beyond the envelope wraps in slow time (a 20 m/s target reads as
−19 m/s), which is genuine pulsed-radar ambiguity; grow
sample_rate_hz, shorten the PRI, or add pulses instead.
Validate and generate¶
rfgen validate --config-dir my-radar-dataset --config-name config
rfgen generate --config-dir my-radar-dataset --config-name config
rfgen inspect ./rfgen-output
Generation writes native Signal Dataset shards and publishes the root last.
Each record carries the response’s components as separate named fields at
native rank, under the projection that produced them:
projections/radar_rx/components/baseband[channel, pulse, fast_time_sample]
is the training field, and .../components/noise sits beside it. Record
metadata carries the typed alignment coordinates, the per-field measurement
planes, the realized backend provenance, and the stamped configured_backend.
radar_noise in that metadata is a measurement plane, not a field name:
the plane labels what the noise component is, and the field is addressed by
its component key.
Under the default sionna_rt backend, baseband is the received signal as a
receiver would record it: the target return plus the chain’s seed-controlled
thermal noise. noise is that same noise realization recorded separately under
its own
measurement plane
label, so a consumer can subtract it for clean-target study or SNR
bookkeeping. Do not add the two: the noise is already in the training
field, and the capture contract’s numeric-sum preflight deliberately
rejects any cross-plane sum. (The validation-only radarsimpy oracle
uses the opposite layout: a noise-free baseband with vendor-drawn noise
beside it, one of the recorded contract differences between engines.)
Inspect the output¶
from rfgen.storage import SignalDatasetStore
access = SignalDatasetStore().open("./rfgen-output")
record = access[0]
# Field names are namespaced by the projection that produced them. The radar
# projection publishes one field per response component under
# `components/`, and `baseband` is the one every backend returns.
print(sorted(record.keys()))
cube = record["projections/radar_rx/components/baseband"]
print(cube.data.shape) # (pulse, fast-time), plus a leading channel axis when present
A target can be absent from the cube for two recorded reasons: the ray
cast found it shadowed by another target (visible: false,
occluded_target_count counts them) or its return failed a signal-chain
gate (dropped_paths). The provenance above is how you tell which.
Content identity derives from record content, not shard placement. The noise draw is chain-owned and seed-controlled, so full records and their content IDs reproduce byte for byte across re-runs of the same configuration and seed.