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 |
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; |
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. |
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 |
|
Single source of truth for fixed value sets; no magic strings in plugin code; YAML stays readable. See Closed enums use |
Active radar |
Coupled response branch behind |
A radar observation couples the transmitted waveform to the received response through a round trip. Forcing it through |
Radar backend policy |
Sionna-first, shipped: |
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 ( |
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
Signalinterchange, and optional classifier-benchmark augmentation. rfgen does not inherit from TorchSig’sBaseSignalGeneratoror 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.scanfor 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 theIQ/Spectrogramaliases).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_jaxadapter), 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 |
|
Channels |
|
Scenes |
|
Labels |
|
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 |
|---|---|---|---|
DAC quantization |
|
||
PA nonlinearity |
|
||
TX phase noise |
|
||
TX IQ imbalance |
|
||
CFO |
|
||
(single propagation slot) |
Channel propagation |
|
BaseChannel directly |
RX mixer |
|
|
|
IF filter |
|
|
|
Resampler |
|
|
|
LNA noise |
|
|
|
ADC quantization |
|
|
|
RX phase noise |
|
|
|
RX IQ imbalance |
|
|
|
AGC |
|
|
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 |
|---|---|---|
|
|
Closed; framework version controls the value set |
|
|
Open; any |
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: stratifieddeserializes to FrequencyPlacementStrategy.STRATIFIED becauseStrEnummembers 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_HARDWARE → FREQUENCY_SHIFT → PROPAGATION → RX_FRONTEND → AWGN) 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
Stageenum with four groups:TX,CHANNEL, and two receiver groups (since removed, as noted above).FREQUENCY_SHIFTdisappears: 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.AWGNdisappears 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_hzasSceneConfig.center_hz + f_offset_hz. Stored SceneMetadata does not mirrorcenter_hz,bandwidth_hz, orsample_rate_hz.SceneConfigstill carries those fields as scene-construction defaults, andReceiverConfig.center_freq_hz,bandwidth_hz, andsample_rate_hzoverride 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 anrx_chainfield on ReceiverConfig. Receiver-specific inputs are threaded through ChannelRxParams.Three sample-rate roles.
R_emitteris per emitter, native, and set by the plugin. The channel rate isSceneConfig.sample_rate_hz: the composer resamples fromR_emitterafter TX impairments and before frequency placement and propagation.R_rxis the effective per-receiver output rate, resolved fromReceiverConfig.sample_rate_hzor, when omitted,SceneConfig.sample_rate_hz. The BaseResamplerStage step on the receiver’s capture plane performs the later transition from the channel rate toR_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_SHIFTwas 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:
Group and Transformation enums in Reference / API / Channels.
Concepts / Channels for the pipeline overview.
Concepts / Coordinate Systems for the frequency frame story.
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_rtbackend 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_rtwas 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_rthas shipped and the configuration guardrail has landed (validation_study: trueis 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.