rfgen.scene¶
The scene-composition layer. Concrete classes orchestrate emitter sampling,
time-frequency placement, channel application, and IQ summation, producing one
composite Signal whose
component_signals carry per-emitter ground truth in the scene’s reference
frame. The shipped rfgen.scene module is a facade over the implemented scene
surface in rfgen.scene_composer, rfgen.placement.base, and
rfgen.config.scene, so from rfgen.scene import DefaultSceneComposer is the
supported import path for the composer and the scene-specific config helpers.
Warning
Pre-implementation. Class signatures, parameter types, and class-attribute
defaults are proposals. Once code lands, this page will be regenerated from
docstrings via sphinx.ext.autodoc. The shape below matches what autodoc emits
so the swap is mechanical.
Module summary¶
import torch
from rfgen.scene import DefaultSceneComposer
from rfgen.config import SceneConfig
composer = DefaultSceneComposer()
scene_cfg = SceneConfig(
sample_rate_hz=20_000_000,
bandwidth_hz=10_000_000,
duration_s=0.020,
)
scene_signal = composer.build(
scene_cfg=scene_cfg,
emitter_pool=emitter_pool,
channel=channel_stack,
rng=torch.Generator().manual_seed(0),
)
The composer’s full algorithm (10 steps, frequency / time placement strategies, overlap policy, multi-RX, chunked composition) is specified in Scene Composition Algorithm. The classes on this page are the public surface that algorithm is exposed through.
Class index¶
Class |
Kind |
Notes |
|---|---|---|
concrete |
Reference BaseSceneComposer implementation; samples emitters, places, applies channels, sums. Fully documented below as the canonical example. |
|
abc |
ABC for scene composers; orchestrates emitter selection, placement, channel application, and IQ summation. |
|
abc |
ABC for time-placement strategies (event timing, hop scheduling, PRI sequences). (stub) |
|
abc |
ABC for frequency-placement strategies (uniform, stratified, ISM-realistic, forced overlap). (stub) |
|
config |
Convenience re-export of rfgen.config.MultiRXConfig for explicit multi-RX layouts or array presets. |
|
config |
Convenience re-export of rfgen.config.ReceiverConfig for per-receiver geometry and RF overrides. |
|
config |
Convenience re-export of the typed scene-frame pose payload used by TX and RX geometry fields. |
|
config |
Convenience re-export of RT solver knobs used under SceneGeometryConfig. |
|
config |
Re-exported scene-asset pointer model (geometry, materials, antenna blobs). |
|
config |
Re-exported geometry-backend selector and overlap-policy carrier. |
For the conceptual map, see Concepts / Scenes. For the executable spec, see Scene Composition Algorithm.
class rfgen.scene.DefaultSceneComposer¶
class DefaultSceneComposer(BaseSceneComposer):
"""Reference scene composer.
Implements the 10-step composition algorithm: per-slot emitter sampling,
weighted class draw, time / frequency placement via pluggable strategies,
per-emitter channel application, scene-level propagation channel, RX
frontend + AWGN, and population of `component_signals` in the scene's
reference frame.
"""
name: str = "default"
Constructor¶
DefaultSceneComposer(
*,
freq_planner_factory: Callable[[], BaseFrequencyPlacement] | None = None,
time_planner_factory: Callable[[], BaseTimePlacement] | None = None,
chunk_threshold_mb: float | None = None,
resampler_name: str = "scipy_poly_resampler",
device_pool_size: int = 10,
scene_cfg: SceneConfig | None = None,
channel: BaseChannel | ChannelPipeline | None = None,
)
Stateless w.r.t. scene contents; the planner factories are how custom
frequency- or time-placement strategies plug in. Both factories are invoked
with no arguments. When omitted, the composer resolves the strategies named in
scene_cfg against the
EntryPointRegistry.
resampler_name and device_pool_size are part of the discoverable
constructor surface and appear in schema(). The optional scene_cfg +
channel pair exists only for construction-time geometry/backend preflight; it
is not part of the YAML-facing schema model.
Class attributes¶
Attribute |
Type |
Value |
Purpose |
|---|---|---|---|
|
str |
|
Registry key. The composer is selected by |
Method: build¶
def build(
self,
*,
scene_cfg: SceneConfig,
emitter_pool: Mapping[str, BaseEmitter],
channel: BaseChannel | ChannelPipeline,
rng: torch.Generator,
) -> Signal
Produces one composite scene. The implementation follows the 10-step algorithm documented in Scene Composition Algorithm; the summary below is the public contract.
Parameters¶
Name |
Type |
Required |
Default |
Description |
|---|---|---|---|---|
|
yes |
– |
Validated scene configuration: bandwidth, sample rate, duration, density, placement strategies, overlap policy, multi-RX geometry, channel-application mode |
|
|
Mapping[str, BaseEmitter] |
yes |
– |
Named pool of concrete emitters. The shipped composer samples this mapping uniformly by key, then samples uniformly from each emitter’s |
|
yes |
– |
Channel application entry point. Pass either a single BaseChannel implementation or a composed ChannelPipeline spanning Group.TX, Group.CHANNEL, Group.RX_CAPTURE, and Group.RX_HARDWARE. The composer dispatches pre-sum transformations per emitter and post-sum transformations per receiver per the Scene Composition Algorithm |
|
|
torch.Generator |
yes |
– |
Master RNG. Per-slot RNGs are derived deterministically per the Determinism reference |
Returns¶
A scene-level Signal:
signal.iq: IQ Shape(2, num_samples)for single-RX,(num_rx, 2, num_samples)for multi-RX, dtypefloat32. When the predicted tensor exceedschunk_threshold_mb, the composer still returns the fully materialized IQ tensor and annotates chunk geometry insignal.metadata.extrasvia{"chunked": true, "chunk_samples": ..., "chunked_signal": ChunkedSignal(...)}.signal.metadata: SceneMetadata Carriesrealized_*audit fields (count, SNR percentiles, class histogram, cochannel-overlap rate, occupancy fraction) plus the resolved scene RNG seed.Signal.metadatais typedUnion[SignalMetadata, SceneMetadata]; at the scene level it is always SceneMetadata.signal.component_signals: tuple of Signal One per placed emitter; each component signal carries its own SignalMetadata withrealized_carrier_hz,start_sample,duration_samples,snr_db,extras["sinr_db"], and RT provenance in the scene’s reference frame. For single-RX RT scenes, the typed provenance lives on SignalMetadata.geometry. For joint multi-RX RT scenes,SignalMetadata.geometrystill carries a canonical GeometryProvenance object for the RT solve, identified byextras["rt_geometry_provenance_canonical_rx_id"]; receiver-specific pose variants remain underextras["rt_geometry_provenance_by_rx"], and the receiver-invariant shared fields remain mirrored underextras["rt_geometry_provenance_shared"]. For geometry-backed RT scenes, scalar scene-levelsnr_db/sinr_dblabels are marked unavailable unless the propagation backend publishes stable receiver-resolved metrics; the composer will not fabricate physically precise scalar labels from pre-propagation energy.
Raises¶
SceneError if
scene_cfgis not a validated SceneConfig instance.SceneError if the propagation backend’s
requires_geometryflag disagrees with the presence of a 3D site geometry inscene_cfg, or if an RT scene reaches propagation without the required typed poses or scene-geometry assets.
Zero-emitter scenes are valid. The composer returns a scene-level
Signal with empty component_signals
and zeroed audit metrics rather than raising.
Example¶
import torch
from rfgen.scene import DefaultSceneComposer
from rfgen.config import SceneConfig
from rfgen.emitters import ChirpRadarEmitter
from rfgen.propagation import AWGNChannel
composer = DefaultSceneComposer()
scene_cfg = SceneConfig(
sample_rate_hz=25_000_000,
bandwidth_hz=10_000_000,
duration_s=0.020,
)
scene_signal = composer.build(
scene_cfg=scene_cfg,
emitter_pool={"radar.lfm": ChirpRadarEmitter()},
channel=AWGNChannel(),
rng=torch.Generator().manual_seed(42),
)
scene_meta = scene_signal.metadata
component_metas = [c.metadata for c in scene_signal.component_signals]
assert scene_signal.iq.shape[-1] == int(round(scene_meta.duration_s * scene_cfg.sample_rate_hz))
assert len(component_metas) == scene_meta.realized_emitter_count
assert all(
m.start_sample + m.duration_samples <= scene_signal.iq.shape[-1]
for m in component_metas
)
This is the geometry-free path: AWGNChannel has requires_geometry = False, and
scene_cfg declares no geometry/assets, so the composer never touches Sionna.
ChirpRadarEmitter’s default bandwidth_hz is 10 MHz, so sample_rate_hz must
clear Nyquist with margin (25 MHz here, not 20 MHz: 10 MHz is not strictly
less than 20e6 / 2). See the SionnaRT / 3D geometry example
below for the ray-traced path with a real scene asset.
Record-length rule: the shipped composer sizes the initial master buffer and,
when the post-sum chain preserves duration, the final scene IQ with
int(round(sample_rate_hz * duration_s)). Python’s round() is the contract,
so exact .5 ties go to the nearest even integer.
Example: SionnaRT / 3D geometry¶
Ray tracing needs a 3D scene asset, a positive absolute RF frequency, and
typed TX/RX poses: all optional for the geometry-free path above, all
required here. SionnaRT.requires_geometry == True, so scene_cfg.geometry
must declare SceneGeometryBackend.SIONNA_RT and scene_cfg.assets must
resolve to a loadable scene:
import torch
from rfgen.scene import DefaultSceneComposer
from rfgen.config import SceneConfig
from rfgen.config.scene import (
GeometryPoseConfig,
MultiRXConfig,
ReceiverConfig,
RTSolverConfig,
SceneAssetsConfig,
SceneGeometryConfig,
)
from rfgen.emitters.torchsig_tone import TorchSigToneEmitter
from rfgen.enums import DensityMode, SceneGeometryBackend
from rfgen.propagation import SionnaRT
composer = DefaultSceneComposer()
scene_cfg = SceneConfig(
sample_rate_hz=2_000_000.0,
bandwidth_hz=1_000_000.0,
duration_s=0.010,
center_hz=2.4e9, # RT needs a positive absolute carrier, not baseband
tx_pose=GeometryPoseConfig(
position_m=(0.0, 0.0, 1.5),
orientation_rad=(0.0, 0.0, 0.0),
),
multi_rx=MultiRXConfig(
receivers=[
ReceiverConfig(
rx_id="rx0",
rx_pose=GeometryPoseConfig(
position_m=(10.0, 0.0, 1.5),
orientation_rad=(0.0, 0.0, 3.14159),
),
)
]
),
geometry=SceneGeometryConfig(
backend=SceneGeometryBackend.SIONNA_RT,
rt_solver=RTSolverConfig(
max_depth=3,
tx_array={"num_rows": 1, "num_cols": 1, "pattern": "iso", "polarization": "V"},
),
),
assets=SceneAssetsConfig(scene_geometry_uri="sionna://builtin/munich"),
)
scene_signal = composer.build(
scene_cfg=scene_cfg,
emitter_pool={"tone": TorchSigToneEmitter()},
channel=SionnaRT(),
rng=torch.Generator().manual_seed(7),
)
center_hz=2.4e9 supplies the positive absolute frequency SionnaRT needs
for scene.frequency; alternatively, set rt_solver.scene_frequency_hz
explicitly and leave center_hz at its baseband default. Each component
signal’s metadata carries the resolved GeometryProvenance and the
extras["rt_channel"] dict (num_paths, dominant_path_gain_linear,
dominant_path_delay_s, and related fields) documented on
SionnaRT in
Reference / API / Propagation.
Method: schema¶
def schema(self) -> type[BaseModel]
Returns the Pydantic model for the composer’s stable constructor kwargs.
Today that surface is chunk_threshold_mb, resampler_name, and
device_pool_size; build() still consumes a separate validated
SceneConfig.
Notes¶
Determinism. Same
(rng_seed, scene_cfg)produces byte-identical IQ. Per-slot RNGs are derived from the masterrngper Determinism; multi-RX RNGs are derived from(scene_seed, rx_idx).Schema validation.
schema()returns the constructor model, notSceneConfig;buildexpects an already-validated SceneConfig.Channel application mode.
scene_cfg.channel_applicationselects between ChannelApplicationMode.SCENE (default for statistical propagation; one propagation call per receiver on the summed master IQ) and ChannelApplicationMode.PER_EMITTER (one call per emitter, summed after). The latter is required for distributed-RX V2X scenes and benefits from RT geometry caching. RT backends that declarerequires_geometry = Truealways fan out per(emitter, RX)call even whenSCENEis selected, because the solve needs a concrete TX pose.Chunked composition. Triggered automatically when
duration_samples * 8 / 1e6 > scene_cfg.chunk_threshold_mb. The current implementation records chunk geometry in metadata for downstream chunk-aware storage, but still materializes the full IQ buffer in memory.component_signalsinvariant. Each component’srealized_carrier_hz,start_sample,duration_samplesare absolute within the scene; the label layer reads these without further transformation.Geometry / propagation cross-validation. At
buildcall time, the composer reads the channel-propagation backend’s (the single slot in Group.CHANNEL)requires_geometry: ClassVar[bool]flag and compares it against the presence of a 3D site geometry block inscene_cfg. Mismatch raises SceneError before any IQ generation. This catches both failure modes: SionnaRT without geometry (hard failure), and a statistical backend with geometry present (silent misconfiguration, also rejected). The shipped implementation has noallow_unused_geometrydowngrade path.
See Also¶
Concepts / Scenes - conceptual framing
Scene Composition Algorithm - full 10-step spec
API Reference / BaseSceneComposer - ABC contract
Config Schema § SceneConfig - full parameter surface
Determinism - per-slot RNG derivation
Compatibility alias: rfgen.scene.OSMSceneBuilder¶
No shipped rfgen.scene.OSMSceneBuilder exists in this branch. Older docs may
still link here; the supported surface is
SceneAssetsConfig plus
SceneGeometryConfig, which let
callers point the scene composer and SionnaRT
at prebuilt geometry assets.
Compatibility alias: rfgen.scene.SceneContext¶
No shipped rfgen.scene.SceneContext exists in this branch. The composer
assembles runtime propagation payloads with
ChannelContext and
ChannelRxParams from
rfgen.channels.protocols; those types carry the typed TX/RX poses, geometry
asset refs, and solver params consumed by the channel layer.
class rfgen.scene.BaseTimePlacement¶
class BaseTimePlacement(ABC):
"""Strategy for placing one emitter in time within a scene."""
name: str
@abstractmethod
def draw(
self, signal: Signal, rng: torch.Generator
) -> list[int]: ...
Kind. Abstract base class.
Returns one start sample for atomic emitters, or many for periodic / event-
timed sources (radar PRI trains, ADS-B 1 Hz beacons, FHSS hop schedules,
Wi-Fi TBTT, BLE advertising). Concrete strategies (iid_uniform,
event_timed, pri_pulse_train, adsb_cadence, fhss_dwells,
wifi_tbtt, ble_advertising) are documented in
Scene Composition Algorithm § Time placement strategies.
Custom strategies plug in by registering a name against the
EntryPointRegistry.
Method: draw¶
@abstractmethod
def draw(self, signal: Signal, rng: torch.Generator) -> list[int]
Returns one or more start-sample positions (in samples at the emitter’s native sample rate) where the emitter’s burst is placed within the scene duration.
Parameters¶
Name |
Type |
Description |
|---|---|---|
|
The emitter’s IQ burst; |
|
|
|
All randomness for this placement is drawn from |
Returns¶
A list[int] of zero-indexed start samples. Each value s is an absolute sample offset within the scene duration such that s + signal.metadata.duration_samples <= scene_duration_samples. For an atomic emitter (one burst), the list has exactly one element. For periodic emitters (PRI trains, beacons), the list has one element per repetition.
Extension contract¶
Non-negative. Every returned start sample MUST be
>= 0.In-bounds. Every returned start sample
sMUST satisfys + signal.metadata.duration_samples <= scene_duration_samples(the scene’s total sample count atsignal.metadata.sample_rate_hz).Non-empty. MUST return at least one sample. An empty list is an error; the shipped composer surfaces this as SceneError.
Deterministic. Same
(signal, rng)state MUST produce the same list. All randomness MUST come fromrng.No in-place mutation. MUST NOT modify
signalor its metadata.
Design note: why no placed argument¶
draw() does not receive a placed list of already-scheduled emitters. Time placement strategies draw each emitter’s position independently. Time overlap detection is the scene composer’s job: after time placement, the composer filters candidate starts under scene_cfg.geometry.overlap_policy. Strategies are not responsible for avoiding time conflicts.
This differs from BaseFrequencyPlacement.draw(), which does receive placed because some frequency strategies need minimum-spacing awareness or forced-overlap targeting at draw time (the composer cannot retry frequency placement without this information). The asymmetry is intentional: frequency placement is context-aware by design; time placement is always independent.
Custom time strategies that want to avoid specific time positions should model that constraint internally (e.g., by using a deterministic grid rather than rejection sampling against placed).
Extension points¶
Method |
Status |
|---|---|
|
Must override |
|
Must set as ClassVar |
class rfgen.scene.BaseFrequencyPlacement¶
class BaseFrequencyPlacement(ABC):
"""Strategy for placing one emitter in frequency within a scene."""
name: str
@abstractmethod
def draw(
self,
signal: Signal,
scene_bandwidth_hz: float,
rng: torch.Generator,
*,
placed: Sequence["Signal"] = (),
) -> float: ...
Warning
Pre-implementation. This page describes proposed contracts. Class signatures, parameter types, config field names, and behavior are subject to change before code lands. Once implementation exists, content here will be regenerated from docstrings or sourced from running tests.
Kind. Abstract base class.
Returns the absolute carrier frequency in Hz for one emitter. Concrete strategies (iid_uniform, stratified, clustered, ism_realistic, forced_overlap) are documented in Scene Composition Algorithm § Frequency placement strategies. Custom strategies plug in by registering a name against the EntryPointRegistry under the rfgen.freq_placement group.
Method: draw¶
@abstractmethod
def draw(
self,
signal: Signal,
scene_bandwidth_hz: float,
rng: torch.Generator,
*,
placed: Sequence["Signal"] = (),
) -> float
Returns the absolute carrier frequency in Hz at which the emitter’s burst will be placed within the scene.
Parameters¶
Name |
Type |
Description |
|---|---|---|
|
The emitter’s IQ burst; |
|
|
float |
Total scene bandwidth in Hz, centered at 0 Hz (baseband). The strategy uses this to enforce the in-band invariant. |
|
|
All randomness for this placement is drawn from |
|
|
Signals already placed in this scene (default: empty). Strategies that enforce minimum spacing ( |
Returns¶
A float absolute carrier frequency in Hz. The returned value is the emitter’s realized_carrier_hz after placement.
Extension contract¶
In-band. The returned carrier
f_cMUST satisfy|f_c| + signal.metadata.bandwidth_hz / 2 <= scene_bandwidth_hz / 2, ensuring the emitter’s occupied band fits within the scene. Strategies that cannot place the emitter in-band (e.g., the scene is too narrow for the emitter) should raise SceneError.Finite. The returned value MUST be finite (not
nan, notinf).Deterministic. Same
(signal, scene_bandwidth_hz, rng, placed)state MUST produce the same carrier. All randomness MUST come fromrng.No in-place mutation. MUST NOT modify
signal, its metadata, or any element ofplaced.
Extension points¶
Method |
Status |
|---|---|
|
Must override |
|
Must set as ClassVar |
Compatibility alias: rfgen.scene.SceneOverlapPolicyConfig¶
No shipped rfgen.scene.SceneOverlapPolicyConfig exists in this branch.
Overlap handling is configured by the enum field
SceneGeometryConfig.overlap_policy on
SceneGeometryConfig. Older
docs that mention p_overlap, retry_budget, or margin_hz describe a
superseded proposal, not the implemented rfgen.scene facade.
class rfgen.scene.MultiRXConfig¶
class MultiRXConfig(BaseModel):
geometry: ArrayGeometry | None = None
receivers: list[ReceiverConfig] = Field(default_factory=list)
Convenience re-export of rfgen.config.MultiRXConfig.
The canonical field contract lives on the config page; the rfgen.scene
facade exports the same model so scene-building code can import the composer
and its receiver-layout helpers from one module.
The implemented contract is narrow:
geometryis a closed-enum array preset.receiversis an explicit list of ReceiverConfig entries.The two are mutually exclusive.
class rfgen.scene.ReceiverConfig¶
class ReceiverConfig(BaseModel):
rx_id: str
position_m: tuple[float, float, float] = (0.0, 0.0, 0.0)
orientation: tuple[float, float, float, float] = (1.0, 0.0, 0.0, 0.0)
rx_pose: GeometryPoseConfig | None = None
antenna_id: str | None = None
center_freq_hz: float | None = None
bandwidth_hz: float | None = None
sample_rate_hz: float | None = None
noise_figure_db: float = 0.0
Convenience re-export of rfgen.config.ReceiverConfig.
The canonical field contract lives on the config page. The implemented model
stores a stable rx_id, legacy position_m / quaternion orientation, the
derived typed rx_pose, optional RF overrides, and noise_figure_db.
Abstract base classes¶
class rfgen.scene.BaseSceneComposer¶
Orchestrates emitter selection, placement, channel application, and IQ summation. This is the layer that replaces TorchSig’s rectangle-overlap loop.
class BaseSceneComposer(ABC):
"""Composes a multi-emitter scene end to end."""
name: str
@abstractmethod
def build(
self,
*,
scene_cfg: SceneConfig,
emitter_pool: Mapping[str, BaseEmitter],
channel: BaseChannel | ChannelPipeline,
rng: torch.Generator,
) -> Signal:
"""Produce one composite scene.
Steps the implementation MUST perform, in this order
(see Scene Composition Algorithm for the full 10-step spec):
1. Draw scene-level parameters (bandwidth, sample rate, duration, density).
2. Sample the number of emitters from scene_cfg.density.
3. For each emitter slot, sample a class from scene_cfg.emitter_zoo.
4. Sample SNR from scene_cfg.power.
5. Call emitter.generate(...) to get a Signal carrying baseband iq and metadata.
6. Apply Group.TX transformations per emitter via ChannelContext (DAC, PA,
TX phase noise, TX IQ imbalance, CFO).
7. Draw time/frequency placement (BaseTimePlacement, BaseFrequencyPlacement);
apply Group.CHANNEL propagation per (emitter, RX) pair; mix into per-RX buffer.
8. Apply Group.RX_CAPTURE transformations per receiver on summed IQ
(RX mixer, IF filter, resampler, LNA noise).
9. Apply Group.RX_HARDWARE transformations per receiver
(ADC, RX phase noise, RX IQ imbalance, AGC).
10. Return a scene-level Signal whose iq is the per-RX output,
whose metadata is a SceneMetadata, and whose component_signals
tuple carries one Signal per placed emitter (each with its own
SignalMetadata in the scene's reference frame).
The channel argument is normalized before execution. A bare BaseChannel
is wrapped as a one-transformation ChannelPipeline, and an existing
ChannelPipeline is used as-is. The implementation then partitions the
normalized pipeline by transformation.value // 10:
Group.TX (1x) and Group.CHANNEL (2x) run pre-sum per emitter.
Group.RX_CAPTURE (3x) and Group.RX_HARDWARE (4x) run post-sum per receiver.
Implementations MAY parallelize within a scene but MUST be
deterministic given the supplied rng.
"""
Geometry / propagation cross-validation contract. Any BaseSceneComposer implementation MUST cross-validate the propagation backend against the scene geometry config before generating IQ. DefaultSceneComposer performs this check after normalizing channel to a ChannelPipeline, so both sides of the public BaseChannel | ChannelPipeline union follow the same path. The check then reads the normalized pipeline’s channel-propagation backend, that is, the single slot in Group.CHANNEL, and compares its requires_geometry: ClassVar[bool] flag against the presence of a 3D site geometry block in scene_cfg:
requires_geometry=Truewith no 3D geometry block: raise SceneError.requires_geometry=Falsewith a 3D geometry block present: raise SceneError.
This check fires before any IQ generation, not at the first generation call. See Concepts / Scenes / Compatibility for the full table.
build()¶
Abstract method on BaseSceneComposer. Produces one composite scene by sampling emitters, placing them in time and frequency, applying channels, and summing into the scene IQ buffer. See the class block above for the full step-by-step contract.
Error surface¶
The shipped scene-composition error surface is SceneError. The implemented composer uses it for invalid scene/config/channel combinations and does not publish a second scene-specific exception type in this branch.