Design Decisions

The choices that shape the framework, with rationale. Quick reference; deeper rationale for each section follows inline.

Decision

Choice

Rationale

TorchSig boundary

Explicit opt-in adapters; do not fork or use it as a substrate

TorchSig supplies useful waveform builders, record-interchange conventions, and benchmark transforms, but rfgen owns core records, scene composition, propagation, physical RX modeling, labels, and storage. Keeping the dependency at adapters avoids coupling those contracts to upstream internals.

Scene composer

Custom rather than TorchSig’s wideband generator loop

TorchSig’s rectangle-overlap approach is useful prior art but does not provide rfgen’s event timing (PRI, hops, beacons), heavy-tailed power, optional cochannel policy, or multi-RX composition/provenance contract.

Physical propagation

rfgen built-ins / Sionna RT / Sionna PHY (TDL/CDL) / custom

A config selects the physical propagation model, from rfgen’s built-ins to ray-traced site-specific channels. Sionna’s Apply*Channel accepts arbitrary IQ, so emitter generation stays independent. TorchSig is used at separate emitter-generation, data-interoperability, and optional classifier-benchmark-augmentation boundaries.

Hardware impairments

Custom torch module, applied per virtual device

Sionna ships a thin impairment library. We model CFO, SFO, IQ imbalance, phase noise (Leeson), PA nonlinearity (Rapp/Saleh), DAC quantization explicitly. These are the basis of RF fingerprinting.

Bandwidth mode

Single code path; num_signals_max and bandwidth are config knobs

Same engine produces narrowband (TorchSig Sig53-style) and wideband (TorchSigWideband-style) recordings. No separate dataset classes.

Label format

Bbox + per-bin segmentation + per-emitter metadata, all stored

Different downstream heads want different formats. Storing all three avoids regeneration.

Text annotations

Template, then LLM rewrite (RF-GPT / WavCaps pattern); 5 annotation types

Cheap ($10–60 per 100K samples with Gemini Flash), grounded in verified metadata, controllable hallucination. Bulk model + verification subset (ShareGPT4V pattern).

Taxonomy

Hierarchical tags across all source label spaces

Benchmark, capture, and custom-synthesis sources use different label vocabularies. Hierarchical tags (Qwen-Audio pattern) unify them without lossy mapping.

Configuration

Hydra config groups + Pydantic v2 validation

Hydra wins on swappable backends; Pydantic catches invalid configs before expensive runs. Used by NeMo, AudioCraft, lerobot.

Storage

Signal Dataset, the only shipped store

Native records need named multidimensional fields, immutable publication, metadata-only reads, and local/GCS ordinal access. storage.backend stays an open selector so a third-party store registered under rfgen.dataset_stores needs no schema change.

Orchestration

Local generation, bounded Dataproc Serverless, unified annotation

Local and Dataproc generation share native shard publication. Annotation reads the one shipped store; its explicit HDF5 and WebDataset sources were retired with those stores.

Validation

HIL (USRP/HackRF cabled + OTA) on a 1K-sample subset

Pure-sim datasets have no sim2real grounding. HIL validates that synthesized fingerprints/channels match real-radio statistics.

Closed enums

StrEnum from rfgen.core.enums; open-set names stay str

Single source of truth for fixed value sets; no magic strings in plugin code; YAML stays readable. See Closed enums use StrEnum below.

Active radar

Coupled response branch behind BaseRadarResponse; never an emitter through the one-way channel

A radar observation couples the transmitted waveform to the received response through a round trip. Forcing it through BaseEmitter plus one-way propagation would omit the return path or duplicate the backend’s radar equations. See Radar as a coupled response branch below.

Radar backend policy

Sionna-first, shipped: sionna_rt is the default radar backend (ray-cast target visibility, closed-form returns through the rfgen signal chain, bit-reproducible from the seed); RadarSimPy is a validation-only oracle gated behind validation_study: true

RadarSimPy’s engine is closed-source, runtime-license-gated object code, and its noise draw was empirically shown not to honor its own seed; Sionna is Apache-2.0, PyTorch-aligned, and differentiable. See Sionna-first radar backend below.

External scene seams

rfgen writes USD (.usda) and does not read it: an exporter out, a named refusal in

Writing a stage is a projection of a plan this repository already owns. Reading one would require inventing a radio material for every prim, because a USD stage carries visual materials from which permittivity and conductivity cannot be derived, and an invented material silently changes every reflection coefficient in a corpus. See External scene seams below.

Cross-domain convergence

Immutable axis-aware complex captures with explicit measurement planes; three-level record-alignment / sample-grid / numeric-sum compatibility preflights

Comms baseband and dechirped radar beat signals are different measurement planes; records may hold both as named native-rank fields, but sample-wise summation requires complete typed physical-coordinate equality. See Cross-Domain Architecture.

Native Signal Dataset storage

Signal Dataset is the native persisted-record contract for local and Dataproc generation, and the only store rfgen ships. StorageBackend has one member. HDF5 survives only as the fixed-shape export stage, which writes a file from already-generated records rather than backing generation.

What we reuse vs. what we build

Reuse

  • TorchSig: selected comms waveform builders through explicit emitter adapters, in-memory LabeledScene ↔ TorchSig Signal interchange, and optional classifier-benchmark augmentation. rfgen does not inherit from TorchSig’s BaseSignalGenerator or use its rectangle-placement utilities: core records, scene composition, labels, storage, propagation, and physical TX/RX transformations remain rfgen contracts. TorchSig impairments are not a channel backend.

  • Sionna: sionna.rt (ray tracing), sionna.phy.channel (TDL/CDL), Apply*Channel (arbitrary-IQ channel application), 5G NR PHY.

  • 3GPP / ITU: TDL-A/B/C tap definitions, ITU multipath profiles.

  • Protocol stacks: pyModeS (ADS-B bits), gr-lora_sdr / LoRaPHY (LoRa TX), gr-ieee802-11 (Wi-Fi a/g/p), gr-ieee802-15-4 (Zigbee), srsRAN_4G via ZMQ (LTE TX waveforms).

  • Infra: Hydra, Signal Dataset, and h5py for the fixed-shape export stage.

  • Patterns: RF-GPT prompt patterns, WavCaps 3-stage pipeline, RF-Analyzer’s PAES metric.

Build (the framework’s distinctive contribution)

  • Heterogeneous emitter zoo wrapping all of the above under one BaseEmitter-compatible interface.

  • Per-device fingerprint module (CFO + SFO + IQ imbalance + phase noise + PA + DAC).

  • Custom multi-emitter scene composer with event timing, density realism, optional multi-RX.

  • Joint label writer (bbox + segmentation + metadata).

  • Inference-grounded annotation pipeline backed by verified metadata; hallucination control + verifier.

  • Taxonomy unifier across all source datasets.

  • Hydra config groups for the full surface.

  • HIL validation harness.

  • Sim2real benchmark + dataset-statistics auditor.

Tensor library: PyTorch canonical, NumPy at boundaries

torch.Tensor is the canonical tensor dtype across the framework. Every layer of the pipeline produces and consumes torch tensors for IQ, spectrograms, segmentation masks, and any per-sample numeric payload. The canonical IQ alias is IQ = NewType("IQ", torch.Tensor).

Why PyTorch:

  • TorchSig is PyTorch-native, and Sionna 1.x has a PyTorch port (Sionna RT and Sionna PHY). Our two largest backend dependencies are PyTorch-first; matching them removes a conversion step at every layer boundary.

  • Foundation-model training pipelines that consume our datasets are overwhelmingly PyTorch. Same dtype, same device semantics, no bridge code at the read side.

  • Heterogeneous scene shapes (variable emitter counts, variable burst durations, mixed sample rates per emitter) suit PyTorch’s eager mode. JAX-style static-shape tracing would force padding or lax.scan for marginal benefit on data-generation workloads.

  • Tooling adoption: PyTorch-first numerical tooling remains preferred where a retained component needs acceleration.

Why not JAX as the canonical dtype:

  • No JAX-native RF library equivalent to TorchSig; we would reimplement signal generators we get for free elsewhere.

  • Random-key threading, immutable arrays, and traced debugging add onboarding cost without a use-case-specific payoff for batched data generation.

  • Adoption gravity outside Google is concentrated in research labs, not the ML training stacks our datasets feed.

Why not NumPy as the canonical dtype:

  • No native GPU. Sionna RT and large FFTs need GPU to be tractable at production dataset scale.

  • Conversion tax at every torch boundary (training, TorchSig, Sionna PHY).

Where NumPy is correct, by policy:

  • TorchSig interop. TorchSig is numpy at the wire; conversion happens once at the interop boundary.

  • HDF5 persistence. h5py uses NumPy arrays at the storage boundary.

  • Audit and statistics. Aggregate scalar arrays consumed by SciPy. Numpy is the conventional dtype for these consumers.

  • HDF5 export. h5py is numpy-native.

  • Build dependency. TorchSig 2.x requires NumPy 2.x; numpy is a transitive dep.

What this means in practice:

  • Every dataclass field, function signature, and code example in this repo that holds IQ or per-sample tensor data MUST type it as torch.Tensor (or the IQ / Spectrogram aliases).

  • Conversion to numpy happens explicitly via .cpu().numpy() at the boundaries listed above, not implicitly mid-pipeline.

  • If a future consumer needs JAX, the bridge lives at the read side (e.g., a thin torch_to_jax adapter), not upstream in the generator.

ABC Pluggability Principle

The framework owns ABCs as the contract between layers. Concrete backends inherit from ABCs. The hierarchy rule is strict:

  • Allowed: abstract → abstract → concrete. A per-slot abstract subclass under a layer ABC is permitted, and concrete implementations then inherit from the per-slot ABC.

  • Forbidden: concrete → concrete. Subclassing one concrete class with another concrete class breaks the pluggability model. A concrete umbrella subclassed by other concrete classes prevents independent swapping of each backend. The per-slot ABC pattern exists specifically to eliminate this: each backend at a given slot inherits from the slot ABC, not from a sibling backend.

Per-layer ABCs are the layer’s primary contract:

Layer

Primary ABC

Emitters

BaseEmitter

Channels

BaseChannel

Scenes

BaseSceneComposer

Labels

BaseLabeler

Annotations

BaseAnnotator

Storage

BaseDatasetStore

Per-transformation ABCs are allowed where multiple implementations exist or are planned at the same pipeline transformation. They narrow the contract to one transformation slot and pin its Transformation ClassVar. The Channels layer uses this pattern extensively: 14 per-transformation ABCs cover the two pipeline groups (TX, CHANNEL) defined by Group plus the receiver front end. Receiver transformations carry no group at all: they are identified by the Transformation.is_receiver predicate, and their stage families live under BaseReceiverStage rather than under BaseChannel. Each receiver family declares one of the two receiver planes (ReceiverStagePlane.CAPTURE or ReceiverStagePlane.HARDWARE), which position the composer-owned joint receiver-background injection between them.

Per-transformation ABC

Operation

Group or receiver plane

Parent

BaseDACQuantization

DAC quantization

Group.TX

BaseChannel

BasePANonlinearity

PA nonlinearity

Group.TX

BaseChannel

BaseTXPhaseNoise

TX phase noise

Group.TX

BaseChannel

BaseTXIQImbalance

TX IQ imbalance

Group.TX

BaseChannel

BaseCFO

CFO

Group.TX

BaseChannel

(single propagation slot)

Channel propagation

Group.CHANNEL

BaseChannel directly

BaseMixerStage

RX mixer

ReceiverStagePlane.CAPTURE

BaseReceiverStage

BaseIFFilterStage

IF filter

ReceiverStagePlane.CAPTURE

BaseReceiverStage

BaseResamplerStage

Resampler

ReceiverStagePlane.CAPTURE

BaseReceiverStage

BaseThermalNoiseStage

LNA noise

ReceiverStagePlane.CAPTURE

BaseReceiverStage

BaseADCQuantizationStage

ADC quantization

ReceiverStagePlane.HARDWARE

BaseReceiverStage

BaseRXPhaseNoiseStage

RX phase noise

ReceiverStagePlane.HARDWARE

BaseReceiverStage

BaseIQImbalanceStage

RX IQ imbalance

ReceiverStagePlane.HARDWARE

BaseReceiverStage

BaseAGCStage

AGC

ReceiverStagePlane.HARDWARE

BaseReceiverStage

Channel propagation reuses BaseChannel directly because Sionna’s channel objects already bundle path loss, multipath, shadowing, and Doppler; splitting that slot would invent an interface no real backend implements (see Channel pipeline: 4 groups, 14 transformations, no scene rate below).

The Scenes layer uses per-slot ABCs for placement strategies: BaseTimePlacement and BaseFrequencyPlacement under BaseSceneComposer.

Supporting ABCs are part of a layer’s pluggability contract when the layer requires more than one ABC for different lifetimes or roles:

  • Storage: BaseDatasetStore (publication lifecycle) + DatasetAccess (open snapshot). A custom backend implements both.

  • Annotations: BaseAnnotator is the retained prompt/template contract, implemented by MetadataAnnotator. The unified runner owns local direct-Gemini execution and remote Vertex Batch submission with immutable JSONL sidecars. General provider switching remains outside the command-facing surface.

Domain-branch contracts follow the same rule: BaseRadarResponse is the radar branch’s layer ABC (concrete backends such as RadarSimPyResponse inherit from it and register through rfgen.radar_responses), and the BaseSceneRenderer is the supporting contract a projection resolves through rfgen.scene_renderers.

This principle is enforced by documentation convention and by the registry, which rejects registrations that do not inherit from the expected per-slot or per-layer ABC.

Closed enums use StrEnum, open-set names stay str

Every config field whose set of valid values is fixed and owned by the framework is typed as a StrEnum member from rfgen.core.enums, not as Literal[str, ...]. Every config field whose value is a registered plugin name stays str and is resolved through the registry at runtime.

The split:

Field

Type

Why

density.mode, frequency_placement.strategy, time_placement.strategy, overlap.policy, array, pa_model, family, provider, types

StrEnum (DensityMode, FrequencyPlacementStrategy, TimePlacementStrategy, SceneOverlapPolicy, ArrayGeometry, …)

Closed; framework version controls the value set

ExecutorConfig.name, LabelConfig.name, StorageConfig.backend

str

Open; any pip install-supplied plugin can register a new name

StorageConfig.backend sits on the open side even though a StorageBackend StrEnum exists: it is typed StorageBackend | str, so signal_dataset gets a member for spelling and typo-checking while a store registered under the rfgen.dataset_stores entry-point group stays reachable without a schema change.

Why this over Literal[str, ...]:

  • Single source of truth. Each enumeration is defined once in rfgen.core.enums. Renaming a value is a one-file change, not a global grep over plugin code.

  • No magic strings inside the framework. Framework code references FrequencyPlacementStrategy.STRATIFIED, not "stratified". IDE autocomplete works; typos surface at edit time.

  • Pydantic validates at the boundary, identical to Literal. Unknown values are rejected at config load with a clear error.

  • YAML stays readable. strategy: stratified deserializes to FrequencyPlacementStrategy.STRATIFIED because StrEnum members are strings at runtime.

Why registered names stay str: the channel and executor registries are open. A new plugin shipped via pip install rfgen-acme-channel registers name="acme" at import time. Closing that to a StrEnum would force a framework release every time a plugin shipped, defeating the registry.

This convention is rolled out incrementally. The retained configuration and schema authorities are api/config and config-schema.

Channel pipeline: 4 groups, 14 transformations, no scene rate (2026-06-17)

Later amended. The two receiver groups this decision introduced no longer exist. Group has only TX and CHANNEL today, and a receiver transformation is identified by the Transformation.is_receiver predicate rather than by a group. The capture/hardware split survives as ReceiverStagePlane, a stage-family attribute (not a configuration field) whose job is to position the composer-owned joint receiver-background injection between the two planes. The record below is preserved apart from those names.

The channel pipeline was originally a five-stage chain (TX_HARDWAREFREQUENCY_SHIFTPROPAGATIONRX_FRONTENDAWGN) running at one shared scene sample rate. That model carried two anchored assumptions: every emitter shares a baseband with every other emitter in the same scene, and every receiver sees the same band. Both assumptions break down for real customer scenarios (a multi-band device with a 5G tuner at 3.5 GHz and a Wi-Fi tuner at 2.4 GHz on the same chassis), and both are unnecessary even for the homogeneous case.

The redesign drops the shared scene baseband and explicitly names every transformation that happens between an emitter and a stored record.

What changed

  • Five stages → four groups. The decision replaced the old Stage enum with four groups: TX, CHANNEL, and two receiver groups (since removed, as noted above). FREQUENCY_SHIFT disappears: in the new model, carrier frequency is metadata throughout pre-sum stages and is applied as a per-RX mixer at the head of the receiver’s capture plane. AWGN disappears as a standalone stage: thermal noise enters at the LNA on the capture plane, where it physically belongs.

  • Coarse stages → 14 explicit transformations. Each transformation is its own pluggable ABC (BaseDACQuantization, BasePANonlinearity, …, BaseAGCStage). The boundary between transformation and group follows what mature libraries actually expose. rfgen exposes the TX-impairment operations and the receiver’s capture-plane and hardware-plane operations as individual slots. Sionna provides one channel object per scenario that bundles path loss, multipath, shadowing, Doppler, and antenna patterns together; we collapse channel propagation to a single BaseChannel slot to match Sionna’s API rather than invent a sub-stage decomposition no real backend implements.

  • No stored scene RF anchor. After frequency placement, the composer writes each component’s absolute SignalMetadata.realized_carrier_hz as SceneConfig.center_hz + f_offset_hz. Stored SceneMetadata does not mirror center_hz, bandwidth_hz, or sample_rate_hz. SceneConfig still carries those fields as scene-construction defaults, and ReceiverConfig.center_freq_hz, bandwidth_hz, and sample_rate_hz override them per receiver when set. Multi-band devices are first-class because stored records preserve per-emitter carriers and per-receiver tuning, not one scene-wide RF anchor.

  • Scene-level post-sum chain. Group 1 is pre-sum and per emitter. Group 2 follows the configured channel-application mode: it runs per emitter/RX path, or on a summed receiver buffer when the selected backend supports scene mode. The receiver stages are applied per receiver after the combine point, starting at the head of the capture plane, but they come from the single scene-level ChannelPipeline (today as its receiver_stages, held alongside the chain rather than as pipeline entries), not from an rx_chain field on ReceiverConfig. Receiver-specific inputs are threaded through ChannelRxParams.

  • Three sample-rate roles. R_emitter is per emitter, native, and set by the plugin. The channel rate is SceneConfig.sample_rate_hz: the composer resamples from R_emitter after TX impairments and before frequency placement and propagation. R_rx is the effective per-receiver output rate, resolved from ReceiverConfig.sample_rate_hz or, when omitted, SceneConfig.sample_rate_hz. The BaseResamplerStage step on the receiver’s capture plane performs the later transition from the channel rate to R_rx.

Why explicit transformations instead of coarse stages

The previous five-stage model bundled multiple physically distinct operations under one Stage. RX_FRONTEND carried mix, IF filter, ADC sample-rate conversion, LNA noise figure, ADC quantization, RX phase noise, RX IQ imbalance, and AGC, all behind one ABC. Plugin authors wanting to override one piece had to reimplement the whole thing or accept the bundled default.

Explicit transformations expose each operation as its own slot. Plugin authors customize what they need; defaults cover the rest. The cost is more ABCs (14 vs 5); the benefit is precise pluggability and a documentation surface that names every operation a reader can expect to see in the pipeline.

Why one slot for BaseChannel

Sionna RT and the UMa/UMi/RMa Sionna PHY scenario models expose one channel object whose internals can couple path loss, multipath, shadowing, and Doppler. TDL/CDL are instead controlled link-level delay/fading profiles; they do not claim complete deployment path loss or shadowing. Antenna patterns are configured per TX/RX entity, not per channel call. Splitting channel propagation into five sub-transformations would invent an interface no real backend implements; collapsing to one slot matches the upstream APIs. TorchSig fading is deliberately not installed as an rfgen channel object: TorchSig remains at the emitter-generation, data-interoperability, and optional classifier-benchmark-augmentation boundaries. Channel application mode is configurable; the current Sionna per-emitter restriction is documented in Integrations / Sionna.

A custom backend that wants per-effect control writes a BaseChannel subclass and is free to use any internal decomposition.

Why the framework gains from this even without multi-band scenarios

Even single-band, single-RX datasets benefit:

  • A narrowband emitter (BLE at 1 MHz BW) no longer pays the upsampling cost of being squeezed into a wideband scene rate.

  • The framework is honest about what each operation does. FREQUENCY_SHIFT was a legacy of the shared-baseband assumption; in any reasonable physical model it does not exist as a sample-domain operation.

  • The mathematical link between the framework and the literature is direct. Each named transformation maps to an operation an RF engineer would draw on a whiteboard.

Historical note

This dated research record describes why the coarse Stage vocabulary was replaced by the current Group and Transformation contracts. For current product behavior, use the Channels reference and concepts pages.

References:

Radar as a coupled response branch (2026-08-13)

Active radar entered the framework as a domain branch parallel to the communications pipeline, not as another emitter family inside it. The one-way pipeline factors cleanly into emit → propagate → capture because a communications receiver never controls the transmitter it hears. A radar observation breaks that factoring: target delay, Doppler, radar cross section (RCS), array response, receiver noise, and mutual-radar interference are jointly evaluated against the radar system’s own transmitted waveform. Modeling radar as BaseEmitter output pushed through one-way propagation would either omit the return path entirely or require rfgen to implement echo and interference equations that the radar backend already owns, a direct violation of the library-first principle.

The two branches share the layers above and below the physics: the scene (coordinate frame, clock, event schedule, seeds) above, and the capture, receiver-frontend, record-composition, and storage contracts below. The convergence contract is an immutable, axis-aware complex capture carrying an explicit measurement plane; three preflight levels (record alignment, sample-grid compatibility, numeric-sum compatibility) distinguish captures that may coexist in one record, captures that may be compared index by index, and captures whose samples may be numerically summed. This matches the position 3GPP Release 19 took for integrated sensing and communication at the scenario level: one shared scene and channel framework with an explicit target-coupling step, never a second independent simulator (see the Rel-19 ISAC channel-modeling survey, arXiv 2512.03506, and the open 3GPP-compliant ISAC channel simulator, arXiv 2606.07328), and mirrors the shared-scenario, per-domain-chain architecture of mature multi-domain RF tools (MATLAB scenario objects, Ansys STK, Keysight SystemVue). Where 3GPP builds its sensing channel by composing one-way TR 38.901 segments with target-coupling statistics, rfgen instead delegates the whole coupled chain to an established radar engine: a library-first choice, not a claim that the 3GPP construction is unworkable.

Radar-v1’s qualification scope is deliberately narrow (single-transmit, single-receive FMCW point-target responses on CPU), and even that scope is gated on a runtime acceptance gate that has not yet passed. Interference, transmit and receive arrays, and the PMCW/pulsed-LFM waveform kinds remain implemented but validation-rejected until each is runtime-qualified. Reader-facing description: Concepts / Cross-Domain Architecture; implemented contract: Reference / API / Radar Response.

Sionna-first radar backend (decided 2026-08-13)

Status update, same day: shipped. rfgen.radar.chain owns the exact dechirp, cube assembly, and seed-controlled noise; the sionna_rt backend uses a deterministic Mitsuba ray cast for target visibility and composes closed-form point returns through that chain (an earlier stochastic-scattering realization was rejected in review for Rayleigh speckle); the six qualification experiments pass against real Sionna 2.0.1; the cross-backend oracle probe records that the two engines sit in exactly conjugate mixing conventions and differ in baseband noise content; and the validation_study configuration gate rejects radarsimpy outside an explicit validation study. Full datasets reproduce byte for byte across independent CLI runs on the default backend.

RadarSimPy was the initial radar response backend because it is the only maintained Python package we identified with a coupled transmit-reflect-receive IQ simulator covering FMCW, PMCW, and pulsed-LFM waveforms, interference, and array geometry. It is also a supply-chain risk: the published GPL license covers only the Python wrapper, the simulation engine ships as closed-source pre-compiled binaries from a single-maintainer vendor, runtime license files gate functionality by tier (as observed on the vendor’s published tier comparison, 2026-08-13: the unlicensed tier is limited to two targets and one TX/RX channel pair), personal tiers exclude commercial use, and no published terms address rights over generated datasets. Independent review then demonstrated empirically that the artifact’s receiver-noise draw is not controlled by its own seed, so record-level reproducibility cannot be promised on it.

The concluded policy is Sionna-first:

  • Sionna RT becomes the default radar path. A future sionna_rt backend composes the shared ray-tracing engine’s two-way paths with an rfgen-owned, gate-validated radar signal chain (target coupling, dechirp against the system’s own sweep, pulse-grid assembly, explicit noise). Sionna RT (Apache-2.0) computes per-path Doppler for moving scene objects and scattering from meshes with material models. Per-path Doppler covers bulk platform motion only; blade-level micro-Doppler requires an explicit rotating-blade model, demonstrated with externally driven time-varying geometry on a ray tracer in “Micro-Doppler Signature Simulation of Multirotor UAVs Using Ray Tracing” (IEEE 11374154), and with an analytic multi-propeller model in arXiv 2504.05168. Its reflectivity is material-model-driven rather than calibrated-RCS-driven, so the backend must pass its own qualification (range/Doppler causality, an RCS calibration strategy) before any scientific claim. As of this decision, sionna_rt was not yet implemented as a radar response backend (the same name already ships as the scene-geometry selector); the status update above records that it has since shipped as the default.

  • RadarSimPy becomes a validation-only oracle: a registered backend that normal generation does not select now that sionna_rt has shipped and the configuration guardrail has landed (validation_study: true is required to select it). The guardrail is configuration validation that rejects it outside an explicit validation-study setting, and record provenance already names the backend, so any violation is visible in the data. Its oracle role is secondary to analytic closed-form ground truth, and any use inside the company still requires vendor terms that cover internal R&D, which the trial tiers do not clearly grant.

Neither backend changes the framework contract: both must implement the same response ABC, produce the same axis-aware captures, and register through the rfgen.radar_responses entry-point group, discovered through the same registry mechanism as every other pluggable seam. (The name sionna_rt already ships as an rfgen.channels entry point and as the scene-geometry selector; rfgen.radar_responses is a distinct entry-point group, so the names do not collide.) The reader-facing contract, including the seams reserved for a later NVIDIA Omniverse integration (Omniverse is NVIDIA’s 3D world-authoring and simulation platform), is the Radar Response reference.

External scene seams: USD out, no USD in (2026-08-15)

The seams reserved above for a later Omniverse integration were opened this cycle, in one direction only. rfgen writes USD and does not read it. The records below are the decisions that shape stayed on, each with the argument that decided it, so a later cycle can reopen one on its merits rather than by guessing what the first one meant.

The boundary is a refusal, not a converter. The installed Mitsuba cannot read USD in any spelling, so a USD asset handed to the shipped ingest is refused by name before any engine work begins. A converter was the alternative and it was rejected on physics rather than on effort. Tessellating geometry is largely provided by OpenUSD; mapping materials is not, and has no answer: ITURadioMaterial is parameterized by an ITU material type and a thickness, while a USD stage carries UsdPreviewSurface visual parameters from which permittivity and conductivity cannot be derived. A converter would have to invent a radio material for every prim, and an invented material silently changes every reflection coefficient, path loss, and delay spread in a corpus, with nothing downstream reporting an error. That is a validation problem with its own literature, not a seam. What ships is the place it would land: a named contract, the rfgen.geometry_ingests group, and a refusal that names the ingest, the kind, the URI, and the kinds that ingest does read.

Digest scope is a pure function of kind and URI scheme. digest_scope_for takes a GeometryAssetKind and a URI and returns what a digest covers, with no I/O of any kind. Purity is the decision: an answer that depended on whether this process could read the path would make provenance depend on which machine minted it, which is exactly the non-reproducibility minting refuses. The consequence is stated rather than hidden. A kind whose named file composes other files, a Mitsuba XML bundle and a USD stage alike, reports root_file, and editing a referenced mesh without touching the root produces a different world under an unchanged digest.

An explicit kind override may only replace the fall-through. Kind is derived from the URI’s suffix, and suffix sniffing is weak: reading bytes to identify a format is unavailable for gs://, s3://, and https:// by construction. The escape hatch is an optional template field, and it is deliberately not a general override. It may only replace the suffix table’s fall-through answer, and a declared kind that contradicts a suffix the table does recognize is refused rather than honoured. An override that could contradict a recognized suffix would let one configuration disagree with itself about what a file is, and the digest scope would follow the wrong branch silently.

Ingest selection is name-then-assert. A configuration selects an ingest by name from rfgen.geometry_ingests, and the resolved class’s own declaration of which kinds it reads is then asserted against the kind in hand. The name is not trusted to imply capability, and the capability is not searched for by trying loaders until one succeeds. An absent selection means the in-repo Mitsuba ingest, which converts nothing, so an absent value in stored provenance is a disclosure rather than a gap.

USD_STAGE_CONVENTION is a stamped string with a version integer. Sionna works Z-up in metres; a USD stage that does not say otherwise is read Y-up in centimetres, so a stage written without stating the convention hands a consumer a scene lying on its side at a hundredth of its size. Every exported stage authors both explicitly and stamps the convention name and a version integer into customLayerData, beside the rotation convention that was already pinned. The version integer is what lets a later cycle change the convention without making every previously exported stage ambiguous: a consumer reads which one it has rather than assuming the current one.

startTimeCode = 0.0, with the epoch carried as a string. A stage’s time axis could have started at the scene’s own epoch, which would make a time code an absolute instant. It does not, for two reasons. A plan at a Unix-epoch origin of 1.7e9 would author codes near 1.7e12, where a double carries only about 2.4e-4 of resolution in time codes while a viewer’s playback controls quantize to integers, so every authored sample would land on a non-representable code. And discarding the epoch instead would make the inverse mapping non-invertible, breaking the round trip a consumer needs to correlate a rendered frame with an RF capture. So the stage starts at zero and carries rfgen:timeOriginS as a string, Python’s own exact representation, parsed back with the standard library, rather than as a double written by a float formatter this repository does not own: an epoch-scale timestamp in a double has roughly a quarter-microsecond of resolution left, and that is the one number whose loss would be silent and unrecoverable. The epoch round trip is then exact by construction. The time-code mapping’s own round trip is bounded rather than exact, to one ulp of the clock’s magnitude; the measured bound is in Reference / API / Scene. The cost is stated: a time code is an offset, and recovering a wall-clock instant needs the metadata as well as the code.

Prim names are reversibly encoded and the join is by attribute. USD prim names must be valid identifiers and every minted target id contains a hyphen, so a refuse-on-hyphen exporter would refuse every plan rfgen mints that has any target. Tf.MakeValidIdentifier is lossy and can silently merge two distinct ids into one prim. The rule is a reversible - to _ replacement, a refusal for anything that does not rescue, and a collision check within each scope. And the exact id travels unencoded as rfgen:planSystemId and rfgen:planTargetId, so the join from a stage back to a plan is by attribute rather than by prim name and stays exact even where the encoding is not injective, which it is not: three of the four id families are validated only for non-emptiness.

Reader-facing description: Concepts / External Scene Seams; implemented contracts: Reference / API / Scene and Reference / API / Engine.