Scenes¶
The scene layer plans and composes one logical, fixed-duration receiver capture that can contain multiple emitters and one or more receivers. Its result is receiver IQ, in-phase / quadrature complex baseband samples, plus component metadata. A scene is not a persisted dataset item: after labeling it becomes one LabeledScene, which a projection turns into exactly one stored record. Receivers do not split it – each arrives as its own named subtree inside that one record. See Records, Receivers, and Assets for how, and Scene Capture Boundaries for the sample, event, and scene terminology.
A scene decides:
how many emitter slots the plan selects,
which emitter classes are selected,
where each emitter sits in time and frequency,
what power or signal-to-noise ratio (SNR) target each emitter is configured against, sampled from a configured distribution,
which receiver or receiver array observes the scene,
whether physical 3D geometry is needed.
The output is one scene-level Signal;
per-event metadata lives on component_signals[], a list of per-event
Signal objects. Each entry represents one
accepted, placed emitted event. A selected emitter slot can yield no entry when
its time strategy validly returns no starts, or multiple entries when it returns
multiple accepted starts. Entries are ordered by accepted placement, not by a
one-to-one correspondence with the scene-plan slots.
From environment description to generated capture¶
A generation job has several different planning questions. They are related, but none is a substitute for the others:
Question |
Concept and owner |
What it does not decide |
|---|---|---|
Which known participants and directed relationships describe the simulated environment? |
Source system records a typed topology. |
It does not create a population, resolve motion, schedule transmissions, model propagation, or generate IQ. |
Which persistent virtual devices should recur across scenes? |
Device population gives devices stable IDs and fingerprint records. |
It does not describe a link topology, choose event times, or generate a waveform. |
How many systems, devices, and events should a scene contain? |
Hierarchical counts records deterministic count vectors. |
It does not select identities, timing, duration, placement, or waveforms. |
Which emitter family and class should one planned slot use? |
Weighted emitter selection records one seeded categorical choice from caller-owned synthetic sampling priors. |
It does not measure RF occupancy or device prevalence, create the waveform, choose duration, place it, apply a channel, or generate IQ. |
How long should a planned event be? |
Duration policy resolves one policy at class, family, role, or scene scope and records its containment result. |
It does not schedule a start, create a waveform, or compose IQ. |
Why and when does each event begin? |
Event planning records event-start relationships and timing provenance. |
It does not choose a scheduler, duration, containment result, or waveform. |
Start with a source system when an external catalog or configuration already names devices and their allowed directed relationships. Use a device population when a dataset needs the same simulated devices, including their hardware fingerprints, to recur across many captures. Use hierarchical counts when the job needs reproducible scene quantities. Use weighted emitter selection when a job must retain one reproducible family/class decision before waveform creation. Use event planning when it must retain start inputs before the component that owns duration and containment runs. Use duration policy when the job must turn a selected class/family/role and capture boundary into a reproducible sample count before it resolves an event.
For a manual event-artifact workflow, resolve the duration separately for each
unresolved event and pass only the values the event planner requires. For
example, with an unresolved event whose ID is "e0":
from rfgen.planning import DurationPolicyResolver, EventPlanner
unresolved_events = EventPlanner.compile([
{"event_id": "e0", "anchor": "absolute", "start_sample": 0},
])
duration_plan = DurationPolicyResolver.resolve(
{"scene": {"kind": "fixed", "value_s": 0.020, "seed": 7}},
class_name="qpsk",
family="digital",
role="uplink",
sample_rate_hz=20_000_000,
capture_samples=2_000_000,
)
resolved_events = EventPlanner.resolve(
unresolved_events,
{"e0": duration_plan.realized_duration_samples},
contained={"e0": duration_plan.contained},
)
The event_id is the key that connects the independently resolved plan to its
unresolved event. EventPlanner.resolve consumes duration_samples and
contained; it calculates the resulting stop_sample. It does not call
DurationPolicyResolver, infer a policy, or check a capture boundary itself.
This is an application-owned handoff, not an automatic pipeline.
These records provide planning context to a generation job; they are not alternate signal generators. The current public APIs deliberately do not silently transform one record into another or automatically inject them into the composer. A caller connects only the information its job owns and records the resulting provenance. The composer remains responsible for realizing a scene: it selects emitters, generates baseband waveforms, applies channels, and produces receiver IQ and metadata.
flowchart LR
source["Source system\nknown topology"]
population["Device population\nstable virtual devices"]
counts["Hierarchical counts\nscene quantities"]
selection["Weighted selection\nfamily + class"]
duration["Duration policy\nsample extent + containment"]
events["Event planning\nstart provenance"]
composer["Scene composer\nemitters + channels → IQ"]
source -. "job-owned context" .-> composer
population -. "job-owned identity context" .-> composer
counts -. "job-owned quantity context" .-> composer
selection -. "job-owned class context" .-> composer
duration -. "job-owned duration context" .-> composer
events -. "job-owned timing context" .-> composer
The arrows mean that an application may use these records when assembling its own job inputs, not that these planning APIs implement an automatic pipeline. This distinction keeps the ownership boundary explicit: emitters produce waveforms, channels transform them into received signals, and the composer coordinates those two operations.
DurationPolicyResolver is separate from the SceneConfig event_duration field, whose value is an
EventDurationConfig.
The resolver creates a durable, per-event planning record selected by class,
family, role, or scene scope. SceneConfig.event_duration is a composer
configuration value: when set, it applies one fixed duration to every emitter
slot in that composition; when omitted, the composer uses the full scene
duration. Neither API automatically configures or invokes the other.
Scene Configs Become Scene Plans¶
At the architecture level, configs describe dataset distributions and records store concrete realizations. The scene layer is where that distinction becomes visible.
To realize a scene is to draw concrete values for every random field, producing one specific scene from the distribution the config describes. A SceneConfig is a Pydantic config that declares either fixed values or distributions for each scene field. It can be fully fixed, fully stochastic, or a mix:
Scene config field |
If fixed |
If stochastic |
|---|---|---|
Emitter count |
Every scene has the same number of emitter slots. |
Each scene draws a count from fixed-range, uniform, or Poisson density settings defined by the scene-composition contract. |
Emitter pool |
Every slot can name a specific class. |
Slots choose classes from configured weights. |
Placement |
Start sample and carrier offset can be pinned. |
Time and frequency strategies draw positions and may retry for overlap policy. |
Power/SNR |
Each emitter uses one configured target. |
Targets are drawn from a configured distribution and realized after the channel chain. The low-noise-amplifier (LNA) noise transformation T10 on the receiver’s capture plane ( |
Channel realization |
A fixed backend and seed can reproduce one path. |
Per-emitter and per-RX sub-seeds produce independent channel realizations (concrete draws from the channel’s random effects, such as fading or multipath; each receiver and each emitter sees its own independent draw). |
The composer turns those settings into a scene plan: a concrete list of
emitter slots with chosen classes, exact parameters, timing and frequency
placement rules, power targets, device identities, and optional TX/RX
positions. It then records each accepted placement as an emitted event. The
scene-level Signal and its
component_signals[] carry those realized events forward to labels, storage,
and audits.
Default Composer¶
DefaultSceneComposer is the primary scene composer. It plans emitter slots, calls emitter backends (the waveform-generation classes, such as generic modulated emitters, radar-like emitters, or captured playback; see Concepts / Emitters), routes component signals through channel transformations, sums propagated components per receiver, and emits scene-level metadata. It does not implement waveform synthesis or RF propagation physics.
The composer delegates per-emitter placement to two pluggable strategy ABCs.
Shipped strategies are selected by their enum values on SceneConfig; time_placement may also name an installed time-placement plugin. The worked Python form appears in the Minimal Example below.
BaseTimePlacement controls start-sample scheduling for each emitter. Shipped strategies live in TimePlacementStrategy:
IID_UNIFORM(uniform draw over scene duration),EVENT_RADAR_PRI(fixed-interval pulse train with per-pulse Gaussian jitter),EVENT_PERIODIC_BEACON(fixed-cadence timing template),EVENT_BURST(bounded, Pareto-derived generic cadence increments),EVENT_BURST_SELF_EXCITING(self-exciting burst arrivals), andEVENT_FHSS_HOP(fixed-dwell hopping template).BaseFrequencyPlacement controls carrier frequency selection. Shipped strategies live in FrequencyPlacementStrategy:
IID_UNIFORM(uniform draw over scene bandwidth),STRATIFIED(equal-width bin selection with optional per-bin weights),REALISTIC_DENSITY/ISM_REALISTIC(draws from configured channel-grid priors),CLUSTERED(anchor-and-jitter draws), andFORCED_OVERLAP(forces overlap with a previously-placed emitter’s occupied band).
All five frequency strategies use rejection sampling with a fixed budget of 64 retries (_MAX_RETRIES = 64). On exhaustion the strategy raises PlacementError naming the strategy, the exhausted retry budget, and the configured min_spacing_hz. There is no nearest-feasible fallback.
Source: the exact rfgen strategy contract is in Reference / Scene Composition Algorithm § Frequency placement strategies and § Time placement strategies. Timing and channel-grid parameters may be inspired by standards or measurements, but they are generic RF templates: they do not implement protocol state machines, packet formats, hopping rules, or calibrated Wi-Fi, Bluetooth, ADS-B, cellular, or other protocol captures.
To swap among shipped strategies, set SceneConfig.time_placement and
SceneConfig.frequency_placement to the relevant enum members, then place any
strategy-specific kwargs in SceneConfig.time_placement_params and
SceneConfig.frequency_placement_params; no subclass is required. To add a
time strategy not in the enum, subclass the relevant ABC and register the
class under the rfgen.time_placement Python entry-point group. Python
packages declare plugins via entry points. The current config does not select
registered frequency-placement plugins. See How-to / Add a placement strategy
extension contract for the registration boilerplate.
The DefaultSceneComposer
resolves the time-strategy name from SceneConfig at build time. See Scene Composition §
Available Implementations for
the registration contract.
Minimal Example¶
This skeleton exercises both placement strategies with shipped members:
EVENT_PERIODIC_BEACON for a generic periodic timing template and REALISTIC_DENSITY for a configurable 2.4
GHz carrier prior. These placement choices can use standards-inspired parameters,
but do not provide protocol fidelity. The emitter_pool and
channel_chain objects come from the emitter and channel setup for the dataset;
see Concepts / Emitters and Concepts /
Channels. Two further worked examples, sparse i.i.d. uniform and
multi-strategy 100 MHz ISM scene with overlap policy, live in Scene Composition
§ Minimal Examples.
The snippet is a skeleton: emitter_pool, channel_chain, and rng are constructed earlier in the program; see Concepts / Emitters for emitter pools and Concepts / Channels for channel chains. density.poisson_rate=0.4 sets the mean emitter count per scene draw directly (mean 0.4), so each Poisson sample draws roughly 0 or 1 emitter regardless of scene bandwidth.
import torch
from rfgen.config import SceneConfig
from rfgen.core.enums import (
DensityMode,
FrequencyPlacementStrategy,
TimePlacementStrategy,
)
from rfgen.scene import DefaultSceneComposer
composer = DefaultSceneComposer()
scene_signal = composer.build(
scene_cfg=SceneConfig(
sample_rate_hz=20_000_000,
duration_s=0.020,
bandwidth_hz=20_000_000,
# Mean of 0.4 emitters per scene draw (independent of scene bandwidth).
density={"mode": DensityMode.POISSON, "poisson_rate": 0.4},
frequency_placement=FrequencyPlacementStrategy.REALISTIC_DENSITY,
frequency_placement_params={"taxonomy": "wifi-2.4ghz"},
time_placement=TimePlacementStrategy.EVENT_PERIODIC_BEACON,
time_placement_params={"period_seconds": 0.1024},
),
emitter_pool=emitter_pool,
channel=channel_chain,
rng=torch.Generator().manual_seed(0),
)
# scene_signal.iq has shape (n_rx_antennas, n_samples); here (2, 400_000).
assert scene_signal.iq.shape == (2, 400_000)
for component in scene_signal.component_signals:
print(
component.metadata.class_name,
component.metadata.start_sample,
component.metadata.realized_carrier_hz,
component.metadata.snr_db,
)
Composition and Geometry¶
Scenes have four separable concerns:
Concern |
What it covers |
When it matters |
|---|---|---|
Scene-plan realization, emitter count, class choice, time-frequency placement, SNR/power priors, overlap, multi-RX, streaming |
Always |
|
The meanings of sample, event, and scene; fixed receiver captures and propagation tails at the capture edge |
Always |
|
3D assets, materials, antenna arrays, TX/RX positions for Sionna RT |
Only for site-specific ray tracing |
|
Where a scene meets USD and Omniverse: asset naming, the ingestion refusal, the stage clock, and the plan-to-stage exporter |
Only when naming a USD asset or exporting a plan to an external 3D tool |
Statistical scenes do not need 3D geometry. They use composition plus a statistical propagation backend such as SionnaUMa (3rd Generation Partnership Project, or 3GPP, urban macro), SionnaUMi (3GPP urban micro), SionnaRMa (3GPP rural macro), SionnaTDL (tapped-delay-line fading), or SionnaCDL (clustered-delay-line fading). The scenario-name acronyms come from 3GPP TR 38.901; see Concepts / Channels for the full backend table.
Geometric scenes use the same composition output, then add site-specific geometry consumed by SionnaRT, a ray-tracing (RT) propagation backend.
Compatibility¶
Geometry and propagation backend are compatibility-locked. The scene composer cross-validates the propagation backend against the presence of a 3D site geometry block at construction time. Mismatches fail validation rather than silently falling back.
Propagation backend |
Requires 3D geometry |
Behavior if mismatched |
|---|---|---|
Yes |
Missing geometry is invalid. |
|
Statistical Sionna backends |
No |
Providing site geometry is invalid unless the backend explicitly declares support. |
TorchSig classification augmentation |
No |
An explicit post-generation benchmark adapter; it is not a scene channel. |
Each propagation class declares its geometry contract in the channels API. The composer reads that declaration and validates it against the scene config. Exact exception classes, error messages, and config fields belong in Reference / rfgen.core.pipeline and Reference / Config Schema.
Coordinate Frames¶
TX and RX positions matter only when the propagation backend consumes geometry. Statistical Sionna backends use ad-hoc topology coordinates internally; the ray-tracing backend reads coordinates from a loaded 3D scene asset. The two are not interchangeable and live in different frames:
Backend family |
Position frame |
Where positions live |
|---|---|---|
Statistical |
Topology frame: ad-hoc UE (user equipment, e.g. a phone) and BS (base station, e.g. a cell tower) positions in meters as defined by the selected statistical scenario |
Scene context positions passed to the channel-propagation transformation |
Geometric |
Asset frame: meters in the loaded 3D scene’s coordinate system |
Same scene context fields; the loaded asset defines the origin and axis convention |
The two frames are not interchangeable. A position correct in topology coordinates may be wildly off in the loaded 3D asset’s coordinates (Mitsuba is the rendering library Sionna RT uses for ray tracing; the asset is typically a Mitsuba .xml file). The composer enforces this:
Positions in scene context carry an implicit frame determined by the chosen propagation backend.
Switching from a statistical backend to a ray-tracing backend without re-coordinating positions fails validation. The validator checks position presence jointly with the backend’s geometry contract.
Open question
Today’s scene context carries the position fields tx_positions_m and
rx_positions_m without an explicit coordinate-frame tag. Scene metadata
similarly notes “scene-frame meters” without specifying which scene frame
applies. Disambiguating topology frame from asset frame requires either
frame-tagged position fields or a single position field with frame implied by
the chosen propagation backend plus validation.
Ownership Boundary¶
The scene composer coordinates mature tools and framework records. It does not own waveform or RF physics:
Responsibility |
Owner |
|---|---|
Waveform synthesis |
|
TX impairments, propagation, RX capture, RX hardware (the four channel groups) |
|
Emitter count, class choice, time-frequency placement, overlap, scene-level metadata |
Scene composer |
3D site assets for ray tracing |
|
Bounding boxes and masks |
|
Text annotations |
Scene composition is custom because heterogeneous wideband dataset composition is the framework’s core job. The custom part is orchestration: selecting and placing emitters, driving existing emitter/channel backends, summing propagated components, and preserving metadata. The composer must not reimplement emitter waveforms or RF channel physics.
Composer Flow¶
The default composer runs six grouped stages. The channel-group acronyms (TX impairments, RX capture, RX hardware) and their per-transformation contents are defined in Concepts / Channels:
Plan: realize a concrete scene plan from fixed values, ranges, weights, and distributions; assign per-emitter time, frequency, power/SNR, device identity, and optional position.
Generate: produce per-emitter baseband waveforms via emitter backends. Baseband means complex-valued IQ samples centered at frequency 0 before channel impairments or carrier shift.
Prepare and place: apply TX impairments, resample and frequency-place each emitted waveform, then prepare per-emitter or geometry-backed propagation. Draw and strictly validate each emitted event’s time start from its pre-propagation scene-rate footprint; an accepted start determines where the prepared result is mixed. A statistical backend configured for
sceneapplication remains post-sum.Sum and scene propagation: combine prepared per-emitter results into one scene buffer per receiver. When the selected statistical backend supports scene application, apply it once to that summed buffer.
Post-sum channel: apply RX capture (mix to baseband, intermediate-frequency filter, resample, sum, low-noise-amplifier noise) and RX hardware (analog-to-digital conversion, phase noise, IQ imbalance, automatic gain control) to each receiver’s composite IQ. See Concepts / Channels for the per-transformation pages that expand each acronym.
Output: emit the scene-level Signal with
component_signals[].
flowchart TD
cfg["SceneConfig + emitter pool + channel chain"]
plan["Plan<br/>per-emitter time, freq, power, position"]
gen["Generate<br/>per-emitter baseband waveforms"]
perchan["TX → resample/frequency → prepare per-emitter propagation<br/>then draw strict emitted-event start and mix"]
sum["Sum<br/>combine prepared components; scene-mode statistical propagation post-sum"]
scene_chan["Post-sum channel<br/>RX capture → RX hardware"]
out["Scene-level Signal<br/>with component_signals[]"]
cfg --> plan --> gen --> perchan --> sum --> scene_chan --> out
Statistical scenes and geometric scenes share this flow. Geometry changes the propagation backend’s input: a statistical backend reads scene topology; a ray-tracing backend reads a 3D scene asset. See Scene Geometry.
The full normative algorithm lives in Reference / Scene Composition Algorithm.
Determinism¶
Same (global_seed, shard_id, sample_idx) produces the same scene population and placement. Per-emitter and per-RX sub-seeds derive from the scene seed so that distributed generation remains reproducible. Full seed-flow rules live in Reference / Determinism.
Sionna RT scenes are deterministic up to backend and device limits (for example, GPU floating-point reductions may not be bit-exact across runs). When exact reproducibility matters, use deterministic Sionna/PyTorch settings and record Sionna, PyTorch, CUDA, and scene-asset versions in metadata.
Placing signals in a scene¶
When a workflow needs reproducible system, device, and event quantities before it chooses identities or event timing, use Hierarchical counts. It records count vectors and does not select devices, schedules, or waveforms.
Placement decides where each emitter sits in time (start sample) and frequency (carrier). The framework ships 11 strategies: 5 frequency-placement strategies (uniform, stratified, realistic-density, clustered, forced-overlap) and 6 time-placement strategies (uniform, radar PRI, periodic beacon, bounded Pareto-derived cadence, self-exciting burst, FHSS hop). Built-in values in SceneConfig.time_placement and SceneConfig.frequency_placement coerce to TimePlacementStrategy and FrequencyPlacementStrategy. time_placement additionally accepts the non-empty entry-point name of an installed time-placement plugin; frequency_placement currently accepts only its shipped enum values.
Recipes for generic scene timing and spectral-density patterns live in the Placement Strategy Selection Guide, which records the evidence tier and any standards- or measurement-inspired parameter sources without claiming protocol fidelity.
When candidate placement must be admitted after the waveform’s measured occupied interval is available, use strict occupied-band placement. It wraps a candidate strategy and checks full guarded containment in the receiver capture; it does not replace ordinary placement strategies or estimate the interval itself.
User-defined time strategies register through the rfgen.time_placement Python entry-point group. The composer resolves the selected plugin at construction time, so a third-party time strategy can be used without modifying framework code. The rfgen.freq_placement group can register frequency strategies, but the current SceneConfig.frequency_placement contract does not yet select them.
When a workflow must retain those start inputs as durable causal provenance before an external duration policy is available, use Event planning. It records timing relationships without selecting a scheduler, duration, or containment policy.
See Also¶
Scene Composition for placement strategies, SNR/power priors, overlap policies, multi-RX, and streaming.
Scene Capture Boundaries for fixed capture length, emitted-event extent, propagation tails, and label semantics at the receiver boundary.
Scene Geometry for 3D site assets when ray-traced propagation is in use.
External Scene Seams for what
rfgen export-planwrites, and why a USD asset can be named but not loaded.Compatibility for the geometry/backend locking rules and fail-fast behavior.
Reference /
rfgen.scenefor the scene API.Reference / Scene Composition Algorithm for the full algorithm contract.
Background / Design Decisions § Channel pipeline for the rationale behind per-emitter carriers and per-receiver sample rates that the scene composer assigns.
Background / Design Decisions § ABC Pluggability for the rationale behind concrete-via-ABC composition that scenes use to swap composers and placement strategies.
Concepts / Channels for the channel chain driven by the composer.
Concepts / Labels for how
component_signals[]becomes bounding boxes and masks.
Background-only scenes¶
A scene with a fixed zero emitter count produces ordinary IQ and scene metadata with empty components and labels. It may therefore use an empty emitter zoo; an empty zoo still fails if a configuration can realize an emitter.