Radar response¶
sionna_rt is the default generation backend (see
The sionna_rt backend); radarsimpy is a
validation-only oracle selectable only under validation_study: true.
Sections describing vendor-probe evidence and license tiers apply to the
RadarSimPy adapter alone.
Use rfgen.radar when an active-radar workflow needs a received baseband
cube (a rank-three complex array indexed by channel, pulse, and fast-time
sample), not merely a transmitted one-dimensional waveform. The request
describes the transmitter, receiver array, point targets (idealized
single-point reflectors), at most one optional interfering radar, seed, and
CPU policy with explicit SI units. The response reports the backend-observed
cube and axes.
One configuration produces one radar dataset through the stock CLI: the
radar scene renderer reads each sample’s point targets from that sample’s
scene plan and simulates one response per sample. The rfgen.radar.receiver projection writes each
response’s components as separate native-rank fields. Follow
Generate a coupled radar dataset
for the runnable path (rfgen init radar-response); the separate
chirp-radar Golden Path remains an emitter-branch waveform, not a coupled
response; see
Cross-Domain Architecture.
RadarSimPy is an optional, separately distributed dependency. Importing
rfgen.radar does not import it. Calling
RadarSimPyResponse.simulate
without an installed authorized runtime raises
BackendUnavailableError.
The checked 2026-08-13 macOS ARM probe imported a locally supplied RadarSimPy
15.3.0 artifact; the operator additionally observed its free-tier startup
message, an observation the probe schema does not record. The probe verified
CPU point-target execution, rank-three output, seed behavior, processing
calls, and the bounded memory case, but interference contrast failed; its
overall state is therefore api_only: the backend’s public API executes,
but the runtime acceptance gate below has not passed. That observation makes
no legal or deployment conclusion. Interface fakes test only adapter wiring
and errors.
Data flow¶
Build an immutable
RadarWaveform,RadarReceiver, andRadarSystem.Add one or more
RadarTargetvalues to aRadarResponseRequest.Call a
BaseRadarResponsebackend.Apply an optional
BaseReceiverFrontendto itsComplexCapture.Or let the
radarscene renderer drive the flow: it builds each sample’s system from that sample’s plan, reads the plan’s targets rather than drawing any, simulates one response, applies the optionally configured frontend stages to every component (recording them inrealized["receiver_frontend"]), and hands the result to therfgen.radar.receiverprojection, which persists each component as a separate native field; a capture is not itself a persisted record.The renderer always receives the sample’s minted
ScenePlanasrender’s first argument. See Plan-driven radar.
RadarSimPyResponse constructs RadarSimPy’s public
Transmitter, Receiver, and Radar
objects and calls
sim_radar. The
adapter itself implements no physics; the vendor engine owns its echo,
Doppler, noise, and interference models end to end. The rfgen-owned
alternative is the signal chain below, which any path-producing engine can
drive.
The interf argument is omitted when the request has no interfering system.
RadarSimPy v1 accepts at most one interfering system. Each interferer’s
radar receives a deterministic derived seed (request.seed + index + 1),
recorded in realized["interferer_seeds"]. In the current artifact that
seed feeds only phase-noise generation, which the adapter does not
configure, so it is inert today; the derivation exists so a future
phase-noise-configured path never shares one stream between victim and
interferer.
Common receiver frontend¶
The cross-domain capture container and the receiver-frontend chain both domains
run are documented with the stages themselves, on the
receiver stages page. The radar
path reaches them through the same ReceiverFrontendChain the communications
path does; what follows here is what the radar path does around that chain.
Radar signal chain¶
rfgen.radar.chain is the rfgen-owned half of a radar backend: an engine
reduces its world to two-way propagation paths, and the chain owns
everything downstream. It exists so ray-traced engines compose with an
exactly specified, gate-validated receive path instead of each engine
reimplementing one.
PropagationPath(delay_s, doppler_hz, gain)is the frozen engine seam: absolute two-way delay at the start of the coherent processing interval, Doppler shift at the carrier (positive closing), and the complex voltage gain of the path as a pure propagation ratio.ChannelPathSetgroups paths per (transmit element, receive element) pair, and the chain requires full tx-major channel coverage.paths_for_point_targets(system, targets)is the analytic free-space engine: per-element ranges set the delay, the radial closing rate at the carrier sets the Doppler, and the radar range equation with unit element gains sets the path power. It refuses rotated systems; a directive or rotated system belongs to a geometry-aware engine.dechirped_channel_matrixsynthesizes the exact conjugate-mixing productrx * conj(tx)for thefmcwandpulsed_lfmkinds, with the time-varying delay carried through every term: the signed static beat-slope * tau, the range-Doppler coupling (a closing target under an up-chirp appears at shorter apparent range), the residual video phasepi * slope * tau**2, and the2 * pi * doppler_hz * PRIpulse advance all follow from one phase expression. Each path’s apparent fast-time frequency and time-varying delay are gated at both ends of the coherent processing interval (against half the complex sampling rate and the sweep window), and the fabricated pre-overlap sliver of the fast-time window is bounded by an explicitmax_pre_overlap_fraction; violations raiseValidationError. The chain refuses rather than drops: an engine producing many paths owns its own filtering and records what it dropped before calling the chain. The genuine slow-time Doppler ambiguity beyond1 / (2 * PRI)is the one remaining stated idealization. PMCW code correlation is a recorded follow-up, and the chain rejectspmcwwaveforms explicitly.thermal_noise_matrixdraws the chain’s explicit, seed-controlled noise: input-referredk * T0 * B * Famplified by the configured gain chain and expressed across the configured load, from one seeded CPU torch generator. The same seed reproduces the same records bit for bit, which the vendor engine’s internal draw does not. That generator is seeded throughrfgen.core.rng.seed_generator, the repository-standard seeding, and the change is disclosed here because it re-baselined the radar noise stream. Before it, the generator was seeded withmanual_seed, which initializes MT19937 from the low 32 bits of the seed alone; two records whose seeds agreed there received a bit-identical, index-aligned noise realization. By the birthday bound that is aboutn**2 / 2**33colliding pairs: roughly 116 pairs per million records, 11.6k at 10M, and about 1.16M pairs involving about 2.30M records (n * (1 - exp(-n / 2**32))) at 100M.seed_generatorspends the high bits on a deterministic offset into the stream (STREAM_OFFSET_BITS = 12), lifting the effective stream space from2**32to2**44, so the residual isn**2 / 2**45, about 284 pairs at 100M: a 4096-fold reduction, not an elimination. The claim is scoped. What the fix removes is index-aligned identity. It does not remove shared randomness: two seeds agreeing on the seed word but differing in the offset still draw from one stream at different starting positions, so shifted-copy pairs remain at the pre-fixn**2 / 2**33rate. The dechirped baseband is untouched, because the baseband synthesis consumes no randomness at all; only the additive noise realization moved. A seed below2**32has offset zero and its noise is unchanged, so the change is observable only on the plan-driven and shard-schedule paths, whose seeds come fromderive_projection_seedandseed_forand are 62- and 63-bit. The committed cross-backend oracle-probe artifact (use_cases/signal-atlas/radar-v1/docs/validation/results/sionna-rt-oracle-probe.json) runs at seed 7, so it was re-run against the fixed stream and itssionna_rt,reference, andscenarioblocks came back byte-identical; it is left as generated rather than re-baselined.ClosedFormRadarSignalChain.synthesize(system, channel_paths, seed=..., plan_context=None)assembles the(frame_tx_rx_channel, pulse, fast_time_sample)cube on the same flattened tx-major channel axis the RadarSimPy adapter records, stamps the shared alignment fromrfgen.radar.chain.radar_capture_alignment(both engines share byte-identical geometry, waveform reference, and physical alignment fields; the phase reference and receiver-id prefix name the producing engine, so whole-alignment equality across engines is false by design), and returns the standardbasebandplusnoisecomponent captures. Aplan_contextre-stamps that one alignment, which both components share, with the plan’stime_reference;radar_capture_alignmenttakes that one fact as an optionaltime_referenceparameter.time_origin_sis unconditionally0.0: where the radar frame sits on the scene clock is the observation projection’s fact, carried as split typed coordinates rather than folded into one float. Omitted, the stamp is the plan-less"radar_frame_start", and the alignment is field for field what it was before the scene planner cycle. The scene offset is metadata: it enters no phase arithmetic, and the chain keeps its CPI-relative internal time base.
The chain’s voltage calibration convention is stated exactly in
rfgen.radar.chain.synthesis: a path’s complex amplitude is
sqrt(P_tx_watts * load_resistor_ohm) * chain_gain * path.gain.
The sionna_rt qualification experiments hold realized cube power to
this convention end to end; cross-backend amplitude comparisons against
the vendor oracle remain unqualified because the two engines’ absolute
scales have not been reconciled.
The analytic gates in tests/unit/test_radar_signal_chain.py hold the
chain to physical identities rather than golden values: the signed
static-target beat -slope * 2 * R / c, the range-Doppler coupling
shift, the residual-video-phase inter-target structure, the
moving-target pulse-to-pulse phase increment 4 * pi * v * PRI / lambda, the Doppler sign convention, the noise power against the
configured noise figure, rejection of unrepresentable paths, and
bit-exact seeded reproducibility.
Plan-driven radar¶
A scene plan is the single source of truth for what it covers, and for
radar that is the system pose, the transmit and receive element geometry,
the pulse-repetition schedule, and every target. Two pure projections in
rfgen.radar.plan_projection compose those plan facts with the private
configuration blocks that stay in configuration (waveform shape, receiver
rates, gains, noise figure, load):
scene_plan_to_radar_system(plan, planned_event_id, waveform, receiver)resolves the owning system from the named event, injects that event’spulse_repetition_intervals_sinto the waveform block, converts the planned orientation torotation_deg, and validates the pulse-fit rule (pulse_duration_s <= min(PRI)) here, which is the first place both halves are in hand and therefore the only place the error can name the responsible plan field. The receiver block’s ownelement_locations_mis not read: the plan owns both arrays.scene_plan_to_radar_targets(plan, planned_event_id)maps the plan’s targets in plan order, propagating each from the plan epoch (clock.time_origin_s) to that event’s start aslocation_m + velocity_m_s * (start_s - time_origin_s). Velocity, radar cross section, and scattering phase pass through unchanged.
RadarRenderer.render(plan, context) is the only entry point, and it is
always plan-driven: there is no params-only branch and no targets block on
RadarRendererParams. The plan owns the system geometry, the pulse schedule,
and the targets; the renderer contributes the engine, the waveform and receiver
hardware, and the optional frontend.
plan_request(plan, request_seed=...) builds that sample’s request from the
plan and returns it with a PlanContext. The request seed is
context.seed, which the observation runtime derived from the sample seed
through the projection namespace —
derive_projection_seed(sample_seed, projection_id) in
rfgen.core.observation_seed — so two projections of one sample draw from
independent streams while both remain reproducible from that sample’s seed.
The renderer then builds the plan’s SceneWorld, hands the backend a
PlanContext carrying the plan’s time_reference and that world, and refuses
a backend whose runtime_qualified lacks "shared_world" before any engine
work happens. Plan-driven radar runs on sionna_rt only this
cycle: it is the only backend with a world consumer, so no plan-driven
cross-backend oracle comparison exists and the analytic closed-form gates
carry the qualification alone.
The sionna_rt backend¶
sionna_rt is the ray-traced radar response backend
(rfgen.radar.backends.sionna_rt.SionnaRTRadarResponse, requires the
sionna extra). It divides the physics between two owners. Ray tracing
answers the geometric questions: each requested point target is realized
as a sphere occluder (its radius is the backend’s one parameter,
occlusion_sphere_radius_m) in a Sionna scene, and one deterministic
Mitsuba ray cast per target decides visibility, so a target shadowed by
another is absent from the response and recorded as occluded. The
point-target return itself is closed-form: visible targets synthesize
through the shared analytic path engine and
ClosedFormRadarSignalChain, with delay to the target center, Doppler
at the carrier, the radar-equation amplitude for the configured
rcs_dbsm exactly (rcs_exact: true in provenance), and chain-owned
seeded noise. No direct transmitter-receiver path is synthesized, so
leakage cannot masquerade as a zero-range return. Full responses and
datasets reproduce bit for bit from the request seed. A qualification
round on the previous realization (stochastic diffuse scattering off a
fixed sphere) measured fully developed Rayleigh speckle, a 22 dB
realized-amplitude spread for one configured target; that evidence
forced this design. World geometry supplied through a plan context
decides visibility and nothing else: it contributes no scattering
return, no clutter, no ground bounce, and no multipath, and
extended-target scattering is not part of this cycle.
When a plan_context carries a SceneWorld, that world is what
visibility is resolved against. The backend holds it at the waveform
carrier, places one occluder node per target inside it for exactly the
resolution, and reads each first-hit distance through the world’s own
first_hit_distance query, so a target behind a building is occluded by
the building as well as by another target. The occlusion arithmetic, its
guard band, and the verdict are unchanged and stay in this backend. The
world’s cache key is stamped into realized provenance as world_key,
with world_content_addressed recording how strong that trace is (a
non-content-addressed digest identifies the reference that was loaded
rather than the bytes that came back). Neither field is present when the
context carries no world, so the presence of world_key is itself the
record of which kind of solve ran. With no world the backend builds its
own throwaway scene exactly as before, which is what keeps a plan-less
run byte-identical. A plan-driven radar run always carries a world, so it
always stamps both fields: a template that names no world asset still
builds one, empty, and its key records that emptiness rather than being
absent.
Runtime qualification covers point_targets, cpu, fmcw, and
shared_world. The first three are evidence-backed by the six
qualification experiments in
tests/integration/sionna_rt/test_radar_backend_qualification.py
(range causality in the exact center bin, Doppler causality, an
end-to-end amplitude oracle holding realized cube power to the radar
equation across ranges and RCS values with the exact -40 dB per decade
slope, chain-owned seeded noise, bit-exact determinism, and occlusion)
and the cross-backend oracle artifact
(use_cases/signal-atlas/radar-v1/docs/validation/results/sionna-rt-oracle-probe.json).
The experiments measure the noise-free plane (training field minus the
recorded noise component) where they isolate the target model, and say
so. Rotated systems are rejected at simulate (scene-geometry cycles
own them), and visibility is evaluated from the system location, with
sub-meter element offsets an accepted approximation. shared_world is earned by the live plan-context experiments in
tests/integration/sionna_rt/test_radar_plan_context.py, whose headline
case runs the whole backend against real geometry: one target occluded
by a reflector plate and one clear target of equal radar cross section
at the same range produce a response whose power is exactly the
one-return radar-equation value, while the same pair against the empty
world produces two returns. It is not a string the backend awards itself
for accepting a world. The oracle establishes two facts to keep in mind when comparing
engines: the two backends sit in exactly conjugate fast-time mixing
conventions (both engines’ beat magnitudes now match the analytic
oracle to the same sub-bin residual), and their baseband components
differ in noise content (the chain’s training field includes its seeded
noise, the vendor’s baseband is noise-free with noise separate).
Validation boundary (RadarSimPy adapter)¶
Construction rejects non-finite or non-positive unit values, malformed
timing, empty arrays or targets, pulse overlap, and any device other than
cpu. Kind-specific waveform rules enforced at construction: FMCW and
pulsed-LFM require exactly two frequency endpoints; frequency_times_s
starts at zero and increases strictly; carrier_hz must lie within the
waveform RF edges; PMCW requires a constant carrier and one phase row per
transmitter element; pulse duration must not exceed the smallest PRI; and a
request carries at most one interfering system. Before loading
RadarSimPy or allocating output, the adapter estimates bytes for every required
returned component (baseband, noise, and optional interference) from
pulse count, flattened TX/RX channel count, fast-time samples, and complex64
size. It checks actual component-array bytes again after readback.
max_output_bytes does not bound RadarSimPy’s internal working memory or
process peak; the isolated-process probe owns that qualification.
For the dechirped waveform kinds, receiver sample rate is the beat-signal sampling rate: dechirping compresses each echo to a low-frequency beat far below the transmitted sweep bandwidth, so RFGen does not compare the two directly. PMCW baseband bandwidth is instead set by the code chip rate; its sampling adequacy is part of PMCW runtime qualification. Range/Doppler Nyquist limits depend on the later variant’s slope, timing, and scene envelope; that validation remains with the configured radar plugin rather than inventing an unresolved core default.
RadarTarget.rcs_dbsm is the target’s radar cross section (RCS, its
effective reflective area) in dBsm, meaning decibels relative to one square
metre, and is passed unchanged to RadarSimPy; it may be negative for targets smaller than
one square metre. rcs_dbsm_to_m2 provides the explicitly named conversion
when square metres are required. RadarWaveform validates kind-specific
fields: PMCW uses RadarSimPy channel mod_t/phs fast-time modulation,
while FMCW and pulsed-LFM forbid those fields. Capabilities separately report
implementation support and runtime qualification. runtime_qualified names exactly the capability
tokens whose execution the checked-in probe exercised: point_targets,
cpu, and fmcw. The probe also verified same-seed determinism, which is
evidence rather than a token. PMCW, pulsed-LFM, interference,
transmit-array, and receive-array requests, including any carried by an
interfering system, are therefore rejected at simulate until each is
runtime-qualified. Same-seed determinism is execution-verified for the
baseband component only: the current RadarSimPy artifact’s noise draw is not
controlled by the request seed, an observed vendor limitation recorded in
the delivery evidence. Execution evidence is a weaker claim than the runtime
acceptance gate below, which remains the bar for dataset claims.
Public contract¶
Implementing BaseRadarResponse¶
BaseRadarResponse is the dependency-injection boundary. A concrete class
must define the ParamsModel, implementation_supported, and
runtime_qualified class attributes and implement from_params and
simulate; nothing else is overridable, because the ABC has no other members.
ParamsModel is a Pydantic v2 model; runtime_qualified must be strictly
evidence-backed. from_params(params) accepts an already validated exact
model and returns a reusable instance. simulate(request, *, plan_context=None) has no persistence
side effects, must be deterministic for a fixed request.seed (the
contract’s end state; the current RadarSimPy artifact deviates for the
noise component; see the determinism scope above, a recorded contract
exception), and returns one RadarResponse whose components mapping
contains a "baseband" key holding the same object as capture (object
identity, not equality).
plan_context is keyword-only and optional, and carries what a scene plan
owns: the plan’s derived time_reference and optionally the shared
SceneWorld. It deliberately does not carry the scene clock – a backend
handed an epoch could only fold it into a float that loses the offset it was
meant to preserve. A backend receiving plan_context=None behaves exactly as
it does with no plan, field for field, including the "radar_frame_start" and
0.0 alignment stamps. A backend receiving a context must stamp the plan’s
time_reference on the alignment it returns, and must query the world when one is present; a backend that
cannot consume a world must refuse a context carrying one rather than
accept it and ignore it, because a capture whose verdicts never saw the
world it names is indistinguishable downstream from one whose did. The
caller checks qualification, not the callee, and only for the world: a
context whose world is not None may go only to a backend whose
runtime_qualified contains "shared_world", which
RadarRenderer enforces before any engine work. The parameter is
required in the signature even for a backend that never accepts a
world, which such a backend signals by omitting that token and
refusing. This is a break for an already installed third-party
backend written against the earlier one-argument signature.
Framework code calls build_radar_response(params, backend=...) once while
constructing a source, then injects the returned ABC instance. Backends are
discovered through the public rfgen.radar_responses entry-point group; the
entry-point target must be the BaseRadarResponse subclass itself (a class,
not a zero-argument factory function; this group is stricter than the
generic pattern in Registry). A new backend package registers
itself without framework changes:
[project.entry-points."rfgen.radar_responses"]
my_backend = "my_package.backends:MyRadarResponse"
The renderer’s backend_params mapping is validated against the
selected backend’s ParamsModel exactly; this is how a registered backend
receives its parameters from configuration. An unknown backend name
raises PluginNotFoundError; unknown parameters raise ConfigError. The
adapter delays its optional import until simulate; a missing or unloadable
artifact raises BackendUnavailableError. Invalid units, timing, capability
requests, memory bounds, or backend output raise ValidationError. Variant,
composer, storage, and CLI modules never import RadarSimPy.
The skeleton below illustrates the contract shape; it is not a runnable
example. RadarSimPyResponse is the complete reference implementation.
class Response(BaseRadarResponse):
ParamsModel = Params
implementation_supported = frozenset({"cpu"})
runtime_qualified = frozenset({"cpu"})
@classmethod
def from_params(cls, params: BaseModel) -> "Response":
if not isinstance(params, Params):
raise TypeError("params must be Params")
return cls()
def simulate(
self,
request: RadarResponseRequest,
*,
plan_context: PlanContext | None = None,
) -> RadarResponse:
... # delegate domain physics and normalize to ComplexCapture
Runtime acceptance gate¶
The runtime acceptance gate is the single scientific bar for dataset claims;
runtime_qualified execution evidence never substitutes for it. The gated
suite must use an authorized RadarSimPy artifact and establish all of the
following on CPU: observed rank-three shape and axes, same-seed determinism
(scoped to the baseband component while the recorded vendor noise exception
stands), different-seed noise behavior, interf contrast, isolated peak
resident set size (process memory), target-range and velocity causality, and
receiver-array phase/angle causality. None is inferred from mocked tests.