Scene Composition

Scene composition draws one training example from a configured population. For one sample_idx, it chooses which transmitters appear, when and where their signals land, how strongly they arrive, and what metadata explains the draw.

In contract terms, composition realizes one concrete scene from a scenario distribution: how many emitters appear, which classes they use, where they land in time and frequency, how they overlap, and what power or signal-to-noise ratio (SNR) prior they follow. A prior is the configured distribution or target range that the composer samples before channel effects produce the final received signal.

Composition is independent of 3D geometry. A non-geometric scene can use a statistical propagation backend, meaning a library channel model between each transmitter and receiver with no loaded 3D asset. A geometric scene uses the same placement and orchestration model, plus Scene Geometry for SionnaRT ray tracing (RT).

What Composition Does

The composer turns a scene config, emitter pool, channel chain, and random number generator (RNG) seed into one populated scene-level Signal. The scene config is the typed Python object or YAML-derived settings for the population. The emitter pool is the set of waveform generators available for selection. The channel chain is the configured transmit, propagation, receive-capture, and receiver-hardware pipeline applied to selected signals. The important distinction is:

  • Config / preset: describes a dataset distribution. Fields can be fixed values, ranges, weights, or named distributions.

  • Scene plan: one concrete realization of that distribution for one sample_idx.

  • Scene signal: in-phase/quadrature (IQ) tensor plus metadata produced for this one draw.

In the diagram below, transmit (TX) steps happen before resampling and frequency placement. Per-emitter and geometry-backed propagation is prepared from that emitted waveform before a time start is drawn. Strict containment then validates the pre-propagation emitted footprint, and an accepted start controls where the prepared received result is mixed. Statistical propagation configured for scene application remains post-sum; receive (RX) steps follow receiver summation. Baseband means complex samples centered at 0 Hz with the carrier frequency carried as metadata. component_signals[] is the list of per-event signal records preserved inside the scene output. Each record corresponds to one accepted placement of one emitted event. A selected emitter slot can therefore produce zero records when an allowed-empty time strategy returns no starts, or multiple records when it returns multiple accepted starts; records follow accepted-placement order rather than a one-record-per-slot plan.

        flowchart TD
    cfg["Scenario config<br/>fixed values + ranges + weights"]
    pool["Emitter pool<br/>available waveform classes"]
    seed["Seed<br/>global_seed + shard_id + sample_idx"]
    plan["Realize scene plan<br/>count, classes, params, time, freq, power"]
    gen["Generate components<br/>clean baseband Signal per emitter"]
    tx["Apply TX processing<br/>per-emitter baseband"]
    prep["Resample + frequency placement<br/>prepare per-emitter propagation"]
    place["Draw/validate time start<br/>strict emitted-event fit, then mix prepared IQ"]
    sum["Sum receivers<br/>scene-mode statistical propagation post-sum"]
    post["Apply post-sum channels<br/>RX capture → RX hardware"]
    out["Output scene Signal<br/>component_signals[] + realized metadata"]

    cfg --> plan
    pool --> plan
    seed --> plan
    plan --> gen --> tx --> prep --> place --> sum --> post --> out
    

The phases are:

Phase

What happens

What becomes metadata

Realize scene plan

Choose emitter-slot count, classes, per-slot parameters, time/frequency placement rules, power or SNR target, device identity, optional position.

Selected-slot count, class names, parameter values, placement rules, center offsets, target SNR or power, device IDs, positions.

Generate components

Call emitter backends for clean baseband waveforms.

Per-emitter waveform metadata and source backend provenance.

TX processing

Apply TX impairments to each emitter’s native baseband waveform.

Fingerprint values.

Resample, frequency-place, and prepare propagation

Resample to scene rate, apply the frequency offset, then prepare per-emitter or geometry-backed propagation for each receiver path. The emitted waveform, not any channel-lengthened output, remains the authority for event duration and label extent.

Carrier and propagation profile/path-channel realization IDs.

Draw start and mix

Draw and validate each time start against the actual pre-propagation emitted length. Each accepted start creates one emitted-event record and determines where the already-prepared result is mixed; an allowed-empty strategy creates none.

Start sample and emitted duration for every accepted event.

Sum receivers

The receiver buffers contain the sum of prepared per-emitter results. A statistical propagation backend configured for scene application runs once on this summed buffer.

Overlap outcomes and component-to-scene alignment.

Apply post-sum channels

Apply the nine-stage core RX chain once to each summed receiver buffer: LO error, intended mixing, IF filter, resampling, LNA noise, ADC, phase noise, IQ imbalance, and digital AGC. RX frontend validation records its evidence and boundaries.

Realized receiver state, noise floor, realized SNR.

Output scene signal

Preserve scene IQ and component_signals[], one record per accepted placed/emitted event.

Ground truth consumed by labelers, storage, audits, and annotations.

Composition owns orchestration and metadata. Emitters own waveform generation. Channels own RF impairments, propagation, RX capture, and RX hardware effects.

Use fixed config values when every record should share the same structure. Use ranges, weights, and distributions when the dataset should cover a population of possible scenes. Both modes use the same composer and the same deterministic seed flow.

Compose an Already-Resolved Event

Use an explicit composer plan when an upstream planning job has already chosen an event’s duration and capture interval. The composer then uses derive_rng(run_seed, sample_id, event_id) for that event instead of redrawing its duration or start. The emitter pool and channel pipeline still create the IQ capture.

One scene can contain several atomic TX-side events. Each event gets its own seed key and can fan out through the existing receiver/channel pipeline. The composer writes their durable identity records to artifacts/plans/<scene_id>/composer-provenance.json. Omit composer_plan for ordinary distribution-driven composition; that legacy path is unchanged.

A resolved source plan can also pin each event to a configured emitter key and class, placement-support bandwidth, frequency offset, typed TX pose, and finite TX power. These source/placement values are supplied together; they are checked against the configured emitter pool and scene band before IQ is mixed. The resulting version-2 provenance records the derived absolute carrier. This remains a composition control surface, not a protocol simulator, captured replay, or measured occupied-bandwidth claim.

Input-construction fragment; this does not create a composer, scene, emitter pool, or channel pipeline:

from rfgen.planning import DurationPlan, ResolvedEvent
from rfgen.scene import ComposerEvent, ComposerPlan

plan = ComposerPlan(
    run_seed=73,
    sample_id="sample-0001",
    scene_id="capture-0001",
    events=(ComposerEvent(
        planned_event_id="event-0", system_id="system-0", device_id="device-0",
        transmitter_role="uplink", link_id="link-0", link_direction="device-to-receiver",
        event=ResolvedEvent(event_id="event-0", anchor="absolute", start_sample=100,
                            duration_samples=100, stop_sample=200, contained=True),
        duration=DurationPlan(policy_kind="fixed", min_s=0.00005, max_s=0.00005,
                              seed=7, requested_duration_s=0.00005,
                              realized_duration_samples=100, issuer="scene", contained=True),
    ),),  # add further independently keyed ComposerEvent values as needed
)

# Pass this object as composer_plan=plan to an already configured
# DefaultSceneComposer.build(...) call.

The system, device, role, and link fields identify a planned transmission. They are provenance, not a protocol simulation or a measurement claim. See the Scene API for the exact input and artifact.

Minimal Examples

Python examples use enum members for framework-owned closed choices. YAML can still use enum string values because Pydantic, the validation library that turns config dictionaries or YAML into typed SceneConfig objects, deserializes them at the config boundary. Exact enum members live in Reference / API / Enums.

These examples show the composition call. Build emitter_pool from explicit emitter selectors and channel_chain from the configured channel entries; the PyTorch torch.Generator supplies a deterministic RNG for this one scene draw.

Sparse random scene:

import torch

from rfgen.config import SceneConfig
from rfgen.core.enums import (
    DensityMode,
    FrequencyPlacementStrategy,
    SceneOverlapPolicy,
    TimePlacementStrategy,
)
from rfgen.scene import DefaultSceneComposer

composer = DefaultSceneComposer()

scene = SceneConfig(
    bandwidth_hz=20e6,
    duration_s=0.020,
    # Mean of 0.2 emitters per scene draw (before Poisson variation and min/max clipping).
    density={"mode": DensityMode.POISSON, "poisson_rate": 0.2},
    frequency_placement=FrequencyPlacementStrategy.IID_UNIFORM,
    time_placement=TimePlacementStrategy.IID_UNIFORM,
)

signal = composer.build(
    scene_cfg=scene,
    emitter_pool=emitter_pool,
    channel=channel_chain,
    rng=torch.Generator().manual_seed(0),
)

Generic fixed-duration bursts

Set scene.event_duration when each generated event should occupy only part of a longer capture. This is a scene-wide fixed policy: every selected emitter slot uses the same duration. The composer generates the event at that duration, then applies TX processing, resampling/frequency placement, and per-emitter or geometry-backed propagation preparation. It draws and validates time starts from the emitted scene-rate footprint, then mixes the prepared result at each accepted start; statistical scene-mode propagation and RX processing still run once on the complete capture. Omit the policy to keep full-scene event generation. In either case, time placement is drawn after resampling and every returned start must retain the whole final event; invalid starts fail scene generation rather than cropping the event.

See Scene Capture Boundaries for the precise meanings of sample, event, and scene, and for the fixed-capture policy: emitted events are never cropped, while a channel-induced propagation tail beyond the receiver capture edge is not recorded. Component metadata and labels remain the emitted event extent. Coordinate Systems is the canonical reference for the scene-rate start_sample coordinate and its mapping to per-receiver label coordinates.

import torch

from rfgen.engine.propagation_generic import AWGNChannel
from rfgen.config import SceneConfig
from rfgen.core.enums import DensityMode, TimePlacementStrategy
from rfgen.domains.radar.chirp_emitter import ChirpRadarEmitter
from rfgen.scene import DefaultSceneComposer

composer = DefaultSceneComposer()
scene = SceneConfig(
    sample_rate_hz=25_000_000.0,
    bandwidth_hz=10_000_000.0,
    duration_s=0.002,
    event_duration={"mode": "fixed", "duration_s": 0.0005},
    density={"mode": DensityMode.FIXED, "min_emitters": 1, "max_emitters": 1},
    time_placement=TimePlacementStrategy.IID_UNIFORM,
)

signal = composer.build(
    scene_cfg=scene,
    emitter_pool={"radar.lfm": ChirpRadarEmitter()},
    channel=AWGNChannel(),
    rng=torch.Generator().manual_seed(7),
)

assert all(
    component.metadata.start_sample + component.metadata.duration_samples
    <= signal.iq.shape[-1]
    for component in signal.component_signals
)
print(
    f"scene_samples={signal.iq.shape[-1]}, "
    f"event_samples={signal.component_signals[0].metadata.duration_samples}"
)
# scene_samples=50000, event_samples=12500

This example requires Python with rfgen and its normal torch dependency installed. To verify the fixed-burst composer behavior from a source checkout, run PYTHONPATH=src python -m pytest -q tests/unit/test_scene_composer.py; Test Execution explains the repository’s test commands and environments.

This is a generic RF-emission capability. It does not by itself model Wi-Fi, Bluetooth, drone, or other protocol packet cadence, hopping, framing, or calibrated captures. The resolved event_duration object is part of the normal resolved configuration provenance, so stored records can be traced to the exact fixed-duration policy used.

Standards-inspired industrial, scientific, and medical (ISM) timing-template scene:

The placement settings below use a generic fixed-cadence timing template and a Wi-Fi channel-plan label. They are useful for constructing a non-uniform RF mixture, but they do not reproduce Wi-Fi TBTT behavior, BLE advertising, packet framing, hopping state, device contention, or any other protocol state machine. Use a protocol simulator or a purpose-built emitter when that level of fidelity is required.

composer = DefaultSceneComposer()

scene = SceneConfig(
    bandwidth_hz=100e6,
    duration_s=0.100,
    density={"mode": DensityMode.POISSON, "poisson_rate": 1.5},
    frequency_placement=FrequencyPlacementStrategy.REALISTIC_DENSITY,
    frequency_placement_params={"taxonomy": "wifi-5ghz"},
    time_placement=TimePlacementStrategy.EVENT_PERIODIC_BEACON,
    time_placement_params={"period_seconds": 0.1024},
    geometry={"overlap_policy": SceneOverlapPolicy.ALLOW},
)

signal = composer.build(
    scene_cfg=scene,
    emitter_pool=emitter_pool,
    channel=channel_chain,
    rng=torch.Generator().manual_seed(1),
)

Ray-traced scene composition still uses the same composer. Geometry appears in the scene and propagation configuration, not in a separate composition algorithm.

Available Implementations

DefaultSceneComposer is the shipped built-in composer. It coordinates heterogeneous emitter selection, time-frequency placement, channel routing, summing, and metadata preservation.

The composer interface is extensible: subclassing BaseSceneComposer is the path for a specialized scene-population engine, such as reproducing a specific radio-frequency machine learning (RFML) baseline or delegating population to an upstream system-level simulator, an external tool that decides which devices transmit when. This is not required for normal dataset generation. The contract that any composer must preserve is documented at BaseSceneComposer and on the Scene Composition Algorithm reference page.

The placement strategies are also extensible as standalone plugins:

  • Time placement: subclass BaseTimePlacement and register under the rfgen.time_placement entry-point group.

  • Frequency placement: subclass BaseFrequencyPlacement and register under the rfgen.freq_placement entry-point group.

Custom time-placement strategies do not require a custom composer; they plug in by name through SceneConfig.time_placement, with constructor kwargs in SceneConfig.time_placement_params. Registry construction reserves and injects the scene’s scene_duration_samples (and sample_rate_hz when the constructor declares it); a length-aware plugin must accept and use the former. Conflicting user values fail instead of authorizing capture-edge cropping. A time_planner_factory receives no injected kwargs and must construct its planner with its own context; regardless of the path, the composer validates the resulting starts against the actual event length. The full extension contract, including the required schema() -> type[BaseModel], is in the Scene API. Frequency-placement plugins may be registered, but the current SceneConfig.frequency_placement contract selects only shipped enum values; its parameter field is SceneConfig.frequency_placement_params.

Python entry points are how installed packages advertise plugin classes to rfgen. The Scene API covers the registration workflow.

Why rfgen Owns Heterogeneous Composition

TorchSig-style RFML baselines are an important compatibility target, but rfgen’s scene composer has a broader job: heterogeneous, metadata-rich scenes that can feed detection, segmentation, captioning, reasoning, and downstream training workflows.

In this table, independent and identically distributed (i.i.d.) placement means each draw is sampled independently from the same configured distribution.

Need

Benchmark-style mixture

rfgen composer

Carrier placement

i.i.d. uniform over configured range

i.i.d., stratified, clustered, ISM-realistic, forced-overlap

Time placement

i.i.d. start sample

i.i.d. plus configured generic timing templates

Emitter count

signal probabilities

density-aware count, e.g. fixed, range, or Poisson-mean per scene

Overlap handling

scalar cochannel probability and retry loop

explicit reject / allow / force policies with metadata

Dense-scene failure mode

retry exhaustion can truncate scenes

truncation recorded in metadata; non-rejection policies available

Multi-RX

not the primary artifact shape

optional multi-RX scenes

The output remains compatible with downstream RFML tooling where possible: component_signals[] carries one accepted placed/emitted event’s metadata per record in the scene reference frame.

Placement Strategies

FrequencyPlacementStrategy selects how emitters are placed in frequency. The five shipped members and their ABCs are:

Member

Class

Description

IID_UNIFORM

IIDUniformFreq

Uniform draw over [freq_min_hz, freq_max_hz] with optional min_spacing_hz rejection.

STRATIFIED

StratifiedFreq

Equal-width bin selection with optional per-bin weights.

REALISTIC_DENSITY

RealisticDensityFreq

Draws from the per-family channel plan loaded through a BaseChannelPlanSource. Also accessible as ISM_REALISTIC / ISMRealistic.

CLUSTERED

ClusteredFreq

Picks an anchor from anchors_hz then adds Gaussian jitter.

FORCED_OVERLAP

ForcedOverlap

With probability p_force, draws inside a previously-placed emitter’s occupied band.

All five strategies use rejection sampling with _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.

TimePlacementStrategy selects how bursts are scheduled in time. The six shipped members are:

Member

Class

Description

IID_UNIFORM

IIDUniformTime

Uniform start-sample draw satisfying 0 <= start <= scene_samples - event_samples.

EVENT_RADAR_PRI

EventRadarPRI

Fixed-PRI pulse train with per-pulse scipy.stats.norm jitter.

EVENT_PERIODIC_BEACON

EventPeriodicBeacon

Generic fixed-cadence schedule; phase chosen by numpy.random.Generator.uniform(0, period). A period may be standards-inspired (for example, a TBTT-like interval), but it does not model Wi-Fi, BLE, ADS-B, cellular, or another protocol.

EVENT_BURST

EventBurst

Generic multi-start cadence whose independently capped Pareto-derived on/off increments are summed between starts. The finite caps bound the realized cadence; they do not define event duration or protocol traffic behavior.

EVENT_FHSS_HOP

EventFhssHop

Fixed-dwell FHSS schedule; dwell length drawn from a configured set via numpy.random.Generator.choice.

EVENT_BURST_SELF_EXCITING

EventBurstSelfExciting

Generic Hawkes self-exciting point-process schedule with an exponential kernel. Use it only when empirically clustered event starts are an appropriate synthetic prior; it is not a Wi-Fi, Bluetooth, cellular, radar, or other protocol simulator.

Source: Reference / Scene Composition Algorithm § Frequency placement strategies and § Time placement strategies define the local draw contracts and parameter tables. The following standards and reference texts can motivate generic timing or channel-plan parameters; citing them does not make a generic placement strategy protocol-faithful:

  • IEEE 802.11-2020 specifies target beacon transmission time (TBTT).

  • Bluetooth Core Specification 5.4 describes BLE advertising and Bluetooth hopping.

  • ICAO Annex 10 Volume IV Chapter 3.1.2.8.1.1 describes Mode S extended squitter.

  • Skolnik’s Radar Handbook discusses radar PRI scheduling.

Channel-plan defaults are grounded by per-band JSON files loaded via JsonChannelPlanSource. Shipped bands: wifi-2.4ghz (IEEE 802.11-2020 Table 17-9, non-overlapping channels 1/6/11 with higher weights), wifi-5ghz (IEEE 802.11-2020 Tables 17-12/17-13), ble (Bluetooth Core 5.4 Vol 6 Part B 1.4, advertising channels 0/12/39), lora-us915 (LoRa Alliance RP002-1.0.4 Table 2-3), lora-eu868 (RP002-1.0.4 Table 2-7), adsb-1090mhz (ICAO Annex 10 Volume IV).

Per-strategy parameters and validation rules live in Reference / Scene Composition Algorithm.

Power and SNR Policies

Power is stated one of two ways, and there is no enum selecting between them. ChannelConfig.snr_db_range (default (-10.0, 30.0)) is a relative target: it asks the channel chain to reach a received signal-to-noise ratio after propagation and receiver noise. SceneConfig.tx_power_dbm (float | None, default None) is absolute: it keeps transmit power in physical units and lets the propagation backend determine the received level. A plan-driven activity may also draw tx_power_dbm per sample through its sampling policy, which is what the shipped narrowband-baseline template does.

The realized SNR is determined after propagation and RX-capture LNA noise injection. Composition samples the prior or target; the channel chain realizes the actual received signal and noise floor.

Source: Reference / Scene Composition Algorithm § SNR and power defines the local prior and reconciliation modes. The receiver-noise operation is the T10 LNA-noise transformation documented in Channels / RX Capture, whose point-of-claim source is the thermal-noise relation used by the local noise-floor table.

Overlap Policies

Cochannel overlap means two emitters occupy the same time-frequency rectangle in the same receiver band. SceneOverlapPolicy selects how the composer handles that case. Members: REJECT, ALLOW (probabilistic; per-draw probability lives on the SceneOverlapPolicyConfig’s p_overlap field), FORCE (deliberate cochannel collisions for stress tests).

Overlap decisions are metadata-bearing events. If rejection budgets are exhausted, the composer must record the realized count and truncation reason so downstream consumers know what was generated. The SceneOverlapPolicyConfig sub-model carries the full parameter surface (p_overlap, retry_budget, margin_hz, overlap_target_strategy).

Source: Reference / Scene Composition Algorithm § Overlap policy defines the local rectangle-overlap semantics, retry behavior, and TorchSig compatibility target. TorchSig’s signal type source is the upstream compatibility anchor for time-frequency rectangle metadata.

Multi-RX and Streaming

Multiple-receiver scenes produce IQ with a receiver axis in the tensor. Each receiver gets its own channel and front-end realization, meaning its own propagation result and receiver-side hardware state. Geometry-aware multi-RX uses the same RX positions and arrays consumed by SionnaRT; statistical multi-RX uses the selected statistical backend’s topology and correlation behavior. Output shape conventions live in Records, Receivers, and Assets.

A scene is composed whole, in memory. A ChunkedSignal wrapper and a memory threshold stood here; they recorded chunk geometry without ever streaming, and both are removed. Bound a scene to available memory through scene.duration_s and scene.sample_rate_hz.

Open Questions

  • Measured density priors. Published measured density anchors for industrial, scientific, and medical (ISM) bands, Automatic Dependent Surveillance-Broadcast (ADS-B), LoRaWAN, Narrowband Internet of Things (NB-IoT), Long-Term Evolution for Machines (LTE-M), and vehicle-to-everything (V2X) coexistence need a separate verification pass.

  • 3GPP configuration examples. Sionna-backed UMa, UMi, and RMa examples could bundle topology defaults, array defaults, BS/UE heights, and indoor/outdoor state.

  • Protocol coordination. Carrier-sense, cellular handover, coordinated multi-point, and retransmission behavior remain out of scope until mature upstream protocol simulators are selected.

Determinism

Same (global_seed, shard_id, sample_idx) produces the same scene composition. Per-emitter sub-seeds derive from (scene_seed, "emitter", emitter_idx). Per-RX sub-seeds derive from (scene_seed, "rx", rx_idx).

Determinism must not depend on rejection-loop timing or backend import order. Full seed-flow rules live in Reference / Determinism.

See Also