Enums

Closed-set string enumerations live in rfgen.core.enums as StrEnum types. They appear as the type of every config field whose set of valid values is fixed and owned by the framework: placement modes, antenna geometries, model families, label layouts, and so on.

This page appears first in the API Reference because these enums are primitive vocabulary for later pages. Core Types and Config reference enum members, but the enums themselves do not depend on framework data types.

Why StrEnum and not Literal[str, ...]:

  • YAML / Hydra stays clean. A StrEnum member is a string at runtime, so segmentation_mode: multi_label in a Hydra config deserializes correctly into SegmentationMode.MULTI_LABEL.

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

  • No magic strings inside the framework. Plugin code references SegmentationMode.MULTI_LABEL symbolically. IDE autocomplete works; renaming a value is a single-file change instead of a global grep.

  • Single source of truth. Each enumeration is defined once in rfgen.core.enums and imported wherever it appears.

Open-set names are different: registered plugin names like the name field on ChannelConfig and ExecutorConfig are str, not enums, because anyone can ship a new plugin with pip install and the registry resolves the name at runtime. See background/design-decisions for the full rule.

YAML / Hydra usage

YAML has no enum literal syntax, so configs always use the string value of the enum member. Pydantic v2 coerces by value at validation time, and because StrEnum members are their string values at runtime, the round-trip is transparent:

# configs/labels/multi_label.yaml
labels:
  segmentation_mode: multi_label            # the string value of SegmentationMode.MULTI_LABEL
  segmentation_tie_break: lower_emitter_index
from rfgen.config import LabelConfig
from rfgen.core.enums import SegmentationMode

cfg = LabelConfig.model_validate(yaml_dict["labels"])
cfg.segmentation_mode                                   # SegmentationMode.MULTI_LABEL
cfg.segmentation_mode is SegmentationMode.MULTI_LABEL   # True
cfg.segmentation_mode == "multi_label"                  # True (StrEnum members are strings)

Two pitfalls to avoid:

  • Do not write segmentation_mode: MULTI_LABEL (the member name). Pydantic looks up by value, not by name, so "MULTI_LABEL" is rejected.

  • Do not wrap the enum in a Hydra _target_: block. That pattern is for instantiating classes (channels, emitters), not enum values. The plain-string form is the standard Pydantic + Hydra idiom.

In Python code and in prose, prefer the symbolic form (SegmentationMode.MULTI_LABEL) over the magic string. In YAML, the string is the only form.

Scene composition

class rfgen.core.enums.ChannelApplicationMode

class ChannelApplicationMode(StrEnum):
    SCENE       = "scene"
    PER_EMITTER = "per_emitter"

Selects where the propagation channel runs relative to the per-emitter IQ summation in SceneConfig.

  • SCENE (default): each emitter is frequency-shifted into the wideband scene buffer, all N emitter signals are summed, and a backend that explicitly supports scene mode runs once on the composite IQ. AWGNChannel supports this mode. Configure Sionna with PER_EMITTER: current SionnaRT execution is forced per-emitter by its geometry requirement, while the statistical Sionna backends fail at application if selected in scene mode. See Sionna integration and the Scene Composition Algorithm.

  • PER_EMITTER: propagation is realized independently per emitter, applied per-emitter at scene SR, and the results are summed after. N propagation calls per scene. Required for distributed-TX ray-traced scenes (e.g. V2X intersections, distributed uplinks) where TX positions are spatially diverse enough that each TX has a meaningfully different propagation path.

In YAML, use the string value: channel_application: scene or channel_application: per_emitter.

See Reference / Scene Composition Algorithm § Channel application mode for the full mode-comparison table.

class rfgen.core.enums.DensityMode

class DensityMode(StrEnum):
    FIXED   = "fixed"
    RANGE   = "range"
    POISSON = "poisson"

Selects how the DensityConfig draws an emitter count per scene. See Concepts / Scenes / Composition.

class rfgen.core.enums.FrequencyPlacementStrategy

class FrequencyPlacementStrategy(StrEnum):
    IID_UNIFORM    = "iid_uniform"
    STRATIFIED     = "stratified"
    CLUSTERED         = "clustered"
    REALISTIC_DENSITY = "realistic_density"
    ISM_REALISTIC     = "ism_realistic"
    FORCED_OVERLAP    = "forced_overlap"

Selects how the scene composer places each emitter in frequency. Python code should use members such as FrequencyPlacementStrategy.IID_UNIFORM; YAML uses the value, such as iid_uniform.

class rfgen.core.enums.TimePlacementStrategy

class TimePlacementStrategy(StrEnum):
    IID_UNIFORM               = "iid_uniform"
    EVENT_RADAR_PRI           = "event_radar_pri"
    EVENT_PERIODIC_BEACON     = "event_periodic_beacon"
    EVENT_BURST               = "event_burst"
    EVENT_BURST_SELF_EXCITING = "event_burst_self_exciting"
    EVENT_FHSS_HOP            = "event_fhss_hop"

Selects how the scene composer places each emitter in time. The values correspond one-to-one to the names registered under [project.entry-points."rfgen.time_placement"], so a configuration string selects the strategy without translation. IID_UNIFORM draws independent start times; the five EVENT_* strategies model a protocol’s own cadence — a radar PRI train, a periodic beacon, a burst process, a self-exciting burst process, and an FHSS hop schedule.

class rfgen.core.enums.SceneOverlapPolicy

class SceneOverlapPolicy(StrEnum):
    REJECT = "reject"
    ALLOW  = "allow"
    FORCE  = "force"

Selects how the composer handles time-frequency overlap between placed emitters. The policy is selected by SceneGeometryConfig’s overlap_policy field, which defaults to ALLOW. There is no per-draw overlap probability: ALLOW accepts an overlap wherever placement produces one.

class rfgen.core.enums.ArrayGeometry

class ArrayGeometry(StrEnum):
    ULA       = "ula"
    URA       = "ura"
    ARBITRARY = "arbitrary"

Antenna array geometry kind for MultiRXConfig.geometry. The geometry kind picks the array layout family; per-array element counts and spacings live on the sibling array-shape fields. MultiRXConfig rejects setting geometry together with a non-empty receivers list (the geometry-XOR-receivers validator).

class rfgen.core.enums.SceneGeometryBackend

class SceneGeometryBackend(StrEnum):
    NONE      = "none"
    SIONNA_RT = "sionna_rt"
    MITSUBA   = "mitsuba"

Scene-geometry backend selector for SceneGeometryConfig.backend. Member values are lowercase to match YAML and the rest of the framework’s closed-set config strings. Legacy "SionnaRT" and "Mitsuba" spellings still coerce for backward compatibility. Picking SIONNA_RT requires SceneConfig.assets.scene_geometry_uri; the SionnaRT-requires-assets validator on SceneConfig enforces this and raises with loc=("assets", "scene_geometry_uri").

Channels

class rfgen.core.enums.Group

class Group(IntEnum):
    TX      = 1
    CHANNEL = 2

Pipeline group tag used by ChannelPipeline to validate chain partition membership. Both members are pre-sum: TX runs per emitter, CHANNEL is the propagation slot. See ChannelPipeline for the validation contract.

The enum has two members because the receiver is no longer a pipeline group. Receiver transformations run in exactly one place, the shared receiver frontend chain, so there is no partition left for a group to express. The integers 3 and 4 are retired rather than reused: stored transformation-log entries carry them as pinned constants, and re-issuing them to a new member would make an archived log ambiguous. Ask Transformation.is_receiver instead of testing for a receiver group.

rfgen.core.enums.RECEIVER_TRANSFORMATIONS and RECEIVER_TRANSFORMATION_ORDER

RECEIVER_TRANSFORMATION_ORDER: Final[tuple[Transformation, ...]]
RECEIVER_TRANSFORMATIONS: Final[frozenset[Transformation]]

RECEIVER_TRANSFORMATION_ORDER is the canonical receiver order, read off the Transformation ordinals: RX_LO_FREQUENCY_ERROR, RX_MIXER, IF_FILTER, RESAMPLER, LNA_NOISE, ADC, RX_PHASE_NOISE, RX_IQ_IMB, AGC. It is the ordering authority for communications chains: a ChannelConfig’s receiver entries must appear in this order, monotonic non-decreasing, because the two resampler selectors share ordinal 33 and are legal adjacent. It is deliberately not the authority for the radar capture seam, whose stage order is the RadarFrontendParams construction order.

RECEIVER_TRANSFORMATIONS is the same nine members as a frozenset, named explicitly rather than derived from an integer range, so adding a member is a deliberate act. It backs Transformation.is_receiver.

The capture/hardware split those nine used to carry as two Group members survives as ReceiverStagePlane (CAPTURE, HARDWARE) on the stage families, which is what positions the joint receiver-background injection between the two planes. See Receiver stages.

class rfgen.core.enums.Transformation

class Transformation(IntEnum):
    # TX impairments
    DAC                   = 11
    PA                    = 12
    TX_PHASE_NOISE        = 13
    TX_IQ_IMB             = 14
    CFO                   = 15

    # Channel propagation
    PROPAGATION           = 21

    # Receiver, capture plane
    RX_LO_FREQUENCY_ERROR = 30
    RX_MIXER              = 31
    IF_FILTER             = 32
    RESAMPLER             = 33
    LNA_NOISE             = 34

    # Receiver, hardware plane
    ADC                   = 41
    RX_PHASE_NOISE        = 42
    RX_IQ_IMB             = 43
    AGC                   = 44

Transformation tag pinned on every concrete BaseChannel subclass and every receiver stage family as a ClassVar[Transformation]. The integer value is an implementation detail used by ChannelPipeline to validate monotonic non-decreasing ordering within each chain partition.

Two properties read the encoding:

Property

Returns

Notes

is_receiver

bool

True for exactly the nine receiver members in the 30s and 40s.

group

Group

Group(value // 10) for TX and CHANNEL members. Raises ValidationError for a receiver member, because receiver transformations no longer belong to a pipeline group.

A caller holding an arbitrary transformation must branch on is_receiver before reading .group. Guarding with getattr(transformation, "group", None) does not work: getattr’s default covers a missing attribute, never an exception raised from inside a property.

The 30s/40s split is the receiver plane marker rather than a group. Its authority is ReceiverStagePlane on the stage families; see Receiver stages.

Emitters and channels

class rfgen.core.enums.EmitterFamily

class EmitterFamily(StrEnum):
    COMMS    = "comms"
    RADAR    = "radar"
    DRONE    = "drone"
    IOT      = "iot"
    ADSB     = "adsb"
    CELLULAR = "cellular"
    PNT      = "pnt"

Coarse emitter family used by EmitterFamilyConfig. New families require a framework version bump; emitter plugins within a family do not. PNT waveform and interference implementations remain installable use-case plugins rather than core emitters.

class rfgen.core.enums.LoRaBackend

class LoRaBackend(StrEnum):
    GR_LORA_SDR = "gr-lora-sdr"
    LORA_PHY    = "lora-phy"

LoRa-emitter backend selector consumed by the scene composer when it instantiates a LoRa emitter from the configured zoo. "gr-lora-sdr" selects the default LoRaSdrEmitter (gr-lora_sdr GNU Radio OOT module); "lora-phy" selects the pure-Python fallback LoRaPHYEmitter (loraphy library).

class rfgen.core.enums.PAModel

class PAModel(StrEnum):
    RAPP  = "rapp"
    SALEH = "saleh"

Power-amplifier nonlinearity model on FingerprintConfig. See Reference / Fingerprint Math.

SNR has no distribution selector

There is no SNRDistribution enum and no ChannelConfig.snr_distribution field; ChannelConfig is extra="forbid", so writing one is refused. SNR is set per channel — AWGNChannel(snr_db=...) — and ChannelConfig.snr_db_range bounds what a configuration may request.

Annotations

class rfgen.core.enums.AnnotationType

class AnnotationType(StrEnum):
    CAPTION = "caption"

One member. qa, reasoning, scene_report and contrastive were removed with the templates that produced them: they drew their numbers from a projection that discarded units, and a label that cannot state a bandwidth in hertz is not a training label.

A configuration naming a removed value fails with a sentence rather than a bare enum error, from rfgen.core.enums.RETIRED_ANNOTATION_TYPES. scene_report is the one that was renamed rather than dropped: its declared-evidence template is now caption.declared.v1.

Labels

class rfgen.core.enums.SegmentationMode

class SegmentationMode(StrEnum):
    SINGLE_LABEL = "single_label"
    MULTI_LABEL  = "multi_label"

Segmentation mask layout selector for LabelConfig.segmentation_mode and SegmentationLabeler.

class rfgen.core.enums.SegmentationTieBreak

class SegmentationTieBreak(StrEnum):
    LOWER_EMITTER_INDEX = "lower_emitter_index"

Single-label overlap policy for LabelConfig.segmentation_tie_break and SegmentationLabeler.

Storage

class rfgen.core.enums.StoreMode

class StoreMode(StrEnum):
    READ   = "read"
    WRITE  = "write"
    APPEND = "append"

Declared, and consumed by nothing this build ships. It described the open modes of the retired pluggable store hierarchy; BaseDatasetStore.open(uri) takes no mode, and the surviving store is immutable by construction — shards are written once and a readable root is published last, so there is no truncating or appending open to ask for. The members are kept because the enum is exported, not because a store reads them.

class rfgen.core.enums.StorageBackend

class StorageBackend(StrEnum):
    SIGNAL_DATASET = "signal_dataset"

Store selector on StorageConfig. signal_dataset is the only store rfgen ships, so the enum has one member.

Unlike the other enums on this page, backend is an open selector: a string that is not a known member passes through validation unchanged, so a third-party store registered under the rfgen.dataset_stores entry-point group is reachable without a schema change. The enum names the shipped store; it does not close the field.

class rfgen.core.enums.GeometryAssetKind

class GeometryAssetKind(StrEnum):
    SIONNA_BUILTIN_SCENE        = "sionna_builtin_scene"
    MITSUBA_XML_BUNDLE          = "mitsuba_xml_bundle"
    OPENGERT_MITSUBA_XML_BUNDLE = "opengert_mitsuba_xml_bundle"
    MATERIAL_DB                 = "material_db"
    ANTENNA_PATTERN             = "antenna_pattern"
    DEEPMIMO_EXPORT             = "deepmimo_export"
    USD_STAGE                   = "usd_stage"
    USDZ_PACKAGE                = "usdz_package"

Typed geometry-related asset categories carried on GeometryAssetRef.kind. SIONNA_BUILTIN_SCENE and MITSUBA_XML_BUNDLE/OPENGERT_MITSUBA_XML_BUNDLE are the two kinds SionnaRT resolves into a loadable Sionna scene (see GeometryAssetRef); MATERIAL_DB and ANTENNA_PATTERN identify material-database and antenna-pattern blobs; DEEPMIMO_EXPORT identifies a DeepMIMO dataset export. USD_STAGE names a .usd, .usda, or .usdc root layer and USDZ_PACKAGE names a .usdz package.

A USD asset is nameable, digestible, and refused. It is not loadable. The installed Mitsuba has no USD plugin of any spelling, so there is nothing for an adapter to call, and the framework ships a declared boundary with a precise refusal rather than a converter nobody has qualified. Naming a USD stage as world geometry gets you a correct digest, a correct kind in stored provenance, and a geometry_format_not_ingestible refusal naming the configured ingest, the kind it was handed, the URI, and the kinds that ingest does read. The refusal runs before any engine work, and it runs on the path a configuration can actually take: scene.assets.scene_geometry_uri naming a .usda under the sionna_rt backend is accepted, reaches SionnaRT.apply, and meets this refusal there rather than an earlier, vaguer one.

Not loadable is not the whole sentence: rfgen writes USD. The asymmetry is the point of the seam rather than a gap in it. A minted ScenePlan is exported as a .usda stage by rfgen export-plan, which is a shipped command an operator runs against a configuration or a written plan artifact. What that stage carries is the plan, not a world: no geometry, no materials, and no world asset content, joined back to the generated records by rfgen:planHash. So a USD_STAGE kind names something this framework can produce and cannot consume, and the two halves never meet: nothing exported here is offered to an ingest, and the refusal above is unchanged by the exporter’s existence. See Scene for the exporter and its contract.

Which kinds an engine reads is a declaration rather than a branch in a loader: rfgen.engine.ingest.GeometryIngest carries an ingests set, the shipped SionnaMitsubaIngest declares SIONNA_BUILTIN_SCENE, MITSUBA_XML_BUNDLE, and OPENGERT_MITSUBA_XML_BUNDLE, and the caller checks membership before handing the reference over. DEEPMIMO_EXPORT is deliberately outside that set: a DeepMIMO export is not a Mitsuba scene, and before the declaration existed it would have been handed to the XML parser. An out-of-tree converter registers under the rfgen.geometry_ingests entry-point group and is named in scene.geometry.geometry_ingest.

How a URI becomes a kind

rfgen.scene.asset_refs.scene_geometry_asset_kind decides by scheme first and then by the suffix of the URI’s path component, so a query string cannot change the answer. Suffixes are matched case-insensitively, so world.USDA classifies as USD_STAGE:

URI shape

Kind

opens with sionna://builtin/

SIONNA_BUILTIN_SCENE

path ends .usda, .usdc, .usd

USD_STAGE

path ends .usdz

USDZ_PACKAGE

path ends .xml

MITSUBA_XML_BUNDLE

anything else

MITSUBA_XML_BUNDLE

The last row is the fall-through, and it is what keeps every configuration written before the USD rows existed on the kind it already had.

Suffix sniffing is a weak rule. Reading bytes to identify the format is unavailable for gs://, s3://, and https:// by construction, so the escape hatch is the optional plan.scene_geometry_kind template field, which is accepted only for a URI whose path carries none of the recognized suffixes. On a URI the table classifies positively, a kind that agrees is a redundant restatement and a kind that disagrees is refused with scene_geometry_kind_conflicts_with_suffix rather than silently winning: both facts are authored in one file, so a disagreement is a typo either way, and a local asset is digested from its bytes without reference to its kind, which means one file declared two ways would reach the world cache under one digest naming two different ingest paths. Declaring MATERIAL_DB or ANTENNA_PATTERN as world geometry is refused with scene_geometry_kind_not_a_world.

What a digest covers: digest_scope_for(kind, uri)

rfgen.scene.asset_refs.digest_scope_for is a pure function returning "root_file", "whole_asset", "distribution", or "uri_identity". It is published rather than stored, so the scope is recoverable from any reference a corpus already holds without a single stored byte moving. It is total over every kind and every supported scheme.

What the answer is about. It describes the scope a content-addressed reference to that (kind, uri) pair carries: a statement about the digest rule the scheme selects, not a prediction about one stored reference. There is one branch where the two part company: a local file:// asset whose bytes could not be read where the reference was minted takes the URI-identity stand-in and is marked content_addressed=False, so that reference’s digest covers the URI alone whatever this function says about its kind. The function cannot see that, and deliberately so: it performs no I/O, and an answer that depended on whether this process can read that path would be the non-reproducibility minting refuses. Read ref.content_addressed for the distinction; the honest reading of a root_file or whole_asset answer is “if this reference is content-addressed, this is what its digest covers”.

The scheme decides first, for every kind alike:

URI scheme

Digest

Scope

sionna://builtin/

sha256(kind:uri:sionna=<version>)

distribution

gs://, s3://, https://

sha256(kind:uri), the URI-identity stand-in

uri_identity

file://

sha256(file_bytes)

per kind, below

Then, for a local file:// asset:

Kind

Scope

What the digest covers

SIONNA_BUILTIN_SCENE

root_file

the named file only

MITSUBA_XML_BUNDLE

root_file

the XML file; the .ply meshes it names are outside the digest

OPENGERT_MITSUBA_XML_BUNDLE

root_file

same, an OpenGERT export is Mitsuba XML

USD_STAGE

root_file

the root layer; sublayers, references, and payloads are outside the digest

USDZ_PACKAGE

whole_asset

every byte of the package file

MATERIAL_DB

whole_asset

the file, which names nothing outside itself

ANTENNA_PATTERN

whole_asset

the same

DEEPMIMO_EXPORT

root_file

the named descriptor, beside companion arrays

The root_file rows describe shipped behavior and are stated because they are easy to over-read. Editing a building’s geometry in a .ply without touching the Mitsuba XML that names it produces a different world under an unchanged digest, and the world cache, keyed on the digest, will serve the stale scene. USD_STAGE inherits exactly that limitation, because a USD root layer composes its sublayers and references the same way.

whole_asset for USDZ_PACKAGE means every byte of the package file, and that equals the composed stage for a conforming package, which is what UsdUtils.CreateNewUsdzPackage and any DCC export produce. It is not a conformance check: OpenUSD will happily open a hand-built archive whose root layer references a path outside the package, and for such a package whole_asset over-claims. Verifying self-containment means resolving every composition arc, which needs an asset resolver this framework does not ship.

See Also