rfgen.config

Pydantic v2 models that validate every config the framework consumes. Hydra composes YAML trees and CLI overrides into a single dict; rfgen.config turns that dict into typed, validated objects before a single sample is generated. A failed config never costs compute.

Warning

Pre-implementation. Class signatures, field names, defaults, and validation rules 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.

Closed-set fields below use StrEnum members from rfgen.enums, never plain Literal[...] or str. YAML still reads mode: poisson; Pydantic deserializes to the enum member at validation time. Open-set plugin selectors such as ExecutorConfig.name stay typed str because they resolve through the plugin registry at instantiation time, not at validation. See background/design-decisions § Closed enums use StrEnum.

Sub-package layout

rfgen.config ships as a sub-package so per-area validators live next to the schemas they validate:

Module

Top-level models

rfgen.config.run

RunConfig

rfgen.config.scene

SceneConfig, DensityConfig, RxArrayConfig, ReceiverConfig, MultiRXConfig, SceneAssetsConfig, SceneGeometryConfig

rfgen.config.placement

PlacementConfig

rfgen.config.channel

ChannelConfig, ChannelChainEntry

rfgen.config.emitter

EmitterZooConfig, EmitterFamilyConfig, FingerprintConfig

rfgen.config.storage

StorageConfig

rfgen.config.executor

ExecutorConfig

rfgen.config.annotation

AnnotatorConfig, LLMConfig, AnnotationOrchestratorConfig, and the five typed batch-resource models

rfgen.config.audit

AuditConfig

rfgen.config.credentials

CredentialsConfig

rfgen.config.label

LabelConfig, LabelerSpec

rfgen.config.reference_host

ReferenceHostConfig

rfgen.config._root

GenerationConfig (single composition root)

rfgen.config.__init__ re-exports every top-level model plus the two canonical entry-point helpers below. A documented from rfgen.config import GenerationConfig, RunConfig, ... works regardless of sub-module path.

Module summary

from rfgen.config import GenerationConfig, from_hydra
from omegaconf import OmegaConf

cfg = from_hydra(OmegaConf.load("configs/config.yaml"))
print(cfg.scene.sample_rate_hz, cfg.scene.density.max_emitters)
print(len(cfg.channel.chain), cfg.executor.name)

Top-level GenerationConfig composes the layered groups (emitter zoo, channel, scene, placement, label, annotator, storage) plus executor, optional credentials, and run metadata. Open plugin selection uses a name field that resolves through EntryPointRegistry. The channel slice is the exception in the shipped Layer 9 code path: live channel behavior is selected by ChannelConfig.chain, while the legacy top-level ChannelConfig.name / params / snr_db_range fields are compatibility placeholders pinned to their defaults until a runtime consumer exists.

Class index

Class

Kind

Notes

GenerationConfig

config (root)

Top-level config composing every layer

RunConfig

config

Run metadata: run_id, sample count, shard size, seed

SceneConfig

config

Signal grid, density, geometry, multi-RX layout, assets

DensityConfig

config

Per-scene emitter count distribution

RxArrayConfig

config

Receiver-array preset (Rx count, array geometry, spacing)

ReceiverConfig

config

One explicit receiver in MultiRXConfig.receivers

MultiRXConfig

config

Geometry XOR explicit receiver list

SceneAssetsConfig

config

Scene-asset URIs (geometry blob, materials, antennas)

SceneGeometryConfig

config

Scene-geometry backend selection plus overlap policy

PlacementConfig

config

Grid-source selection for the realistic_density strategy

EmitterZooConfig

config

Pool of emitters and per-class sampling weights

EmitterFamilyConfig

config

One pool entry: family, classes, params, weight, fingerprint

FingerprintConfig

config

Per-device CFO/SFO/IQ-imbalance/phase-noise/PA priors

ChannelConfig

config

Channel chain selection, ordered transformation list, SNR range

ChannelChainEntry

config

One transformation entry in ChannelConfig.chain

LabelConfig

config

Labeler and per-task label parameters

LabelerSpec

config

One entry in LabelConfig.extra_labelers

AnnotatorConfig

config

Inference templating layer config

LLMConfig

config

One inference endpoint (provider, model, sampling params)

AnnotationOrchestratorConfig

config

Phase-2 batch annotation backend and provider resources; separate from GenerationConfig

AuditConfig

config

Dataset-audit thresholds and bounded aggregation limits

StorageConfig

config

Store backend, output URI (required), on-disk layout

ExecutorConfig

config

Distributed executor selection (open-set name)

CredentialsConfig

config

Cloud credential provider selection

ReferenceHostConfig

config

Reference-host metadata for wall-clock and cold-import tests

For the full normative schema (defaults, validators, YAML examples), see Config Schema.


Canonical entry points

Two module-level helpers cover every documented validation path. CLI, dataset-consumer, and validation-and-audit consume one of these two; raw GenerationConfig.model_validate is reserved for tests and internal use.

from_hydra

from rfgen.config import from_hydra, GenerationConfig
from omegaconf import DictConfig

def from_hydra(cfg: DictConfig | dict[str, object]) -> GenerationConfig: ...

Convert a Hydra/OmegaConf node into a validated GenerationConfig. The bridge:

  1. OmegaConf.is_config(cfg) is true → OmegaConf.to_container(cfg, resolve=True) resolves interpolations into a plain dict.

  2. cfg is a plain dict → used as-is (symmetry with validate_config()).

  3. Anything else (including ListConfig, scalar nodes, arbitrary objects) raises ConfigError with context={"input_type": type(cfg).__name__}.

After resolution the top-level container must be a dict. If it is not (for example an OmegaConf list), the helper raises:

ConfigError: Hydra config must be a dict at the top level; got list

with context={"top_level_type": "list"}. The validated dict is then routed through validate_config(), so Pydantic failures surface as ConfigError.

validate_config

from rfgen.config import validate_config

def validate_config(data: object) -> GenerationConfig: ...

Validate a plain dict and surface failures as ConfigError. Implementation:

try:
    return GenerationConfig.model_validate(data)
except ValidationError as exc:
    raise ConfigError(
        message=str(exc),
        context={"errors": json.loads(exc.json())},
    ) from exc

The original pydantic.ValidationError is preserved as the cause. The context payload is JSON-serializable by construction: ValidationError.json() already coerces non-serializable ctx values (raw ValueError instances from custom validators) to strings, so the round-trip through json.loads yields a list[dict] matching Pydantic’s documented error-payload shape.


class rfgen.config.GenerationConfig

class GenerationConfig(BaseModel):
    emitter_zoo: EmitterZooConfig = Field(default_factory=EmitterZooConfig)
    channel: ChannelConfig = Field(default_factory=ChannelConfig)
    scene: SceneConfig = Field(default_factory=SceneConfig)
    placement: PlacementConfig = Field(default_factory=PlacementConfig)
    label: LabelConfig = Field(default_factory=LabelConfig)
    annotator: AnnotatorConfig | None = None
    storage: StorageConfig                              # required
    executor: ExecutorConfig = Field(default_factory=ExecutorConfig)
    credentials: CredentialsConfig | None = None
    run: RunConfig                                      # required

Top-level config. Composes the emitter zoo, channel, scene, placement, label, annotator, and storage groups plus executor, optional credentials, and run metadata. Built by model_validate from the Hydra-resolved dict (typically through from_hydra()). Plugin-name typos surface as PluginNotFoundError at plugin instantiation, not at validation.

model_config = ConfigDict(extra="forbid", validate_assignment=True). Unknown keys are rejected (they are the most common source of silent config drift).

No-override build error

GenerationConfig() (no overrides, no Hydra input) raises pydantic.ValidationError because exactly two leaf fields have no defensible default:

  • run.run_id (no defensible default for a stable run identifier).

  • storage.path (no defensible default for an output URI).

A mode="before" model validator on GenerationConfig seeds empty run and storage sub-dicts when the input omits them entirely, so Pydantic descends into the nested schemas and the resulting error names exactly those two missing leaves rather than naming the parent slots. The contract test asserts:

import pytest
from pydantic import ValidationError
from rfgen.config import GenerationConfig

with pytest.raises(ValidationError) as exc:
    GenerationConfig()

missing = {".".join(str(p) for p in err["loc"]) for err in exc.value.errors()
           if err["type"] == "missing"}
assert missing == {"run.run_id", "storage.path"}

Every other top-level slot has a default_factory or accepts None, so it cannot contribute to a missing-leaf report.

Fields

Field

Type

Required

Default

Description

emitter_zoo

EmitterZooConfig

no

EmitterZooConfig() (empty families)

Pool of emitters with per-family sampling weights. The “non-empty families” check is deferred to the scene composer (Layer 4).

channel

ChannelConfig

no

ChannelConfig()

Channel chain selection. Resolved against the registry to a BaseChannel.

scene

SceneConfig

no

SceneConfig()

IQ tensor shape, density, placement strategies, multi-RX layout, geometry.

placement

PlacementConfig

no

PlacementConfig()

Grid-source selection for the realistic_density frequency placement strategy.

label

LabelConfig

no

LabelConfig()

Primary labeler and per-task label parameters.

annotator

AnnotatorConfig | None

no

None

Inference-grounded annotation block. When None, or when annotator.enabled is False, the annotation phase is skipped.

storage

StorageConfig

yes

Storage backend; storage.path has no defensible default.

executor

ExecutorConfig

no

ExecutorConfig()

Distributed executor selection (open-set name).

credentials

CredentialsConfig | None

no

None

Cloud credential provider; SDKs fall back to default chains when None.

run

RunConfig

yes

Run-level metadata; run.run_id has no defensible default.

Notes

  • Single validation point. Open plugin names such as executor.name are strings at config time; they resolve against the entry-point registry at instantiation, not at model_validate.

  • Phase 1 and Phase 2 use separate schemas. GenerationConfig remains the Phase 1 root and rejects a top-level annotation_orchestrator key. The shipped rfgen annotate command composes AnnotationOrchestratorConfig separately for synchronous local or managed-provider Phase 2 execution.

  • Output path is on storage, not run. The output directory is storage.path; run does not have an output_path field.

  • Credentials are optional. Most managed-compute environments inject ambient credentials.


class rfgen.config.RunConfig

class RunConfig(BaseModel):
    run_id: str = Field(min_length=1)             # required, no default
    num_samples: int = Field(gt=0, default=10_000)
    shard_size: int = Field(gt=0, default=1_000)
    seed: int = 42
    shard_failure_threshold: float = Field(gt=0.0, le=1.0, default=1.0)
    fail_fast: StrictBool = False

Run metadata block. run_id has no default and is one of the two no-default leaves driving the no-override-build error contract on GenerationConfig. The output directory is StorageConfig.path, not a field of RunConfig. A @model_validator(mode="after") warns (does not raise) when num_samples % shard_size != 0; the last shard is allowed to be partial.

Fields

Field

Type

Required

Default

Description

run_id

str (min_length=1)

yes

Stable, human-meaningful identifier for the run. No defensible default. The contract test for GenerationConfig() asserts the raised ValidationError names this leaf.

num_samples

int (> 0)

no

10_000

Total number of Record objects to generate.

shard_size

int (> 0)

no

1_000

Records per shard. The total shard count is ceil(num_samples / shard_size).

seed

int

no

42

Master RNG seed. Per-shard and per-sample seeds derive deterministically from this value plus the shard index.

shard_failure_threshold

float (0.0 < x <= 1.0)

no

1.0

Fraction failed_samples / requested shard samples that causes orchestration to append a shard-level ShardFailureError. The denominator remains the requested shard size when fail_fast leaves later samples unattempted. At the default 1.0, the threshold is reached only when failed_samples equals the requested shard size.

fail_fast

StrictBool

no

False

When True, the shard worker stops on the first per-sample failure. Records yielded before that failure may already be stored, but the worker aborts the shard commit so no shard-complete marker is published; a retry can repair the shard. Numeric and string-like booleans such as 0, 1, "false", and "true" are rejected at config-validation time.


class rfgen.config.SceneConfig

class SceneConfig(BaseModel):
    sample_rate_hz: float = Field(gt=0, default=20_000_000.0)
    duration_s: float = Field(gt=0, default=0.020)
    bandwidth_hz: float = Field(gt=0, default=10_000_000.0)
    center_hz: float = 0.0
    density: DensityConfig = Field(default_factory=DensityConfig)
    time_placement: TimePlacementStrategy = TimePlacementStrategy.IID_UNIFORM
    time_placement_params: dict[str, object] = Field(default_factory=dict)
    frequency_placement: FrequencyPlacementStrategy = FrequencyPlacementStrategy.IID_UNIFORM
    frequency_placement_params: dict[str, object] = Field(default_factory=dict)
    rx_array: RxArrayConfig = Field(default_factory=RxArrayConfig)
    multi_rx: MultiRXConfig | None = None
    channel_application: ChannelApplicationMode = ChannelApplicationMode.SCENE
    geometry: SceneGeometryConfig = Field(default_factory=SceneGeometryConfig)
    assets: SceneAssetsConfig = Field(default_factory=SceneAssetsConfig)

Top-level scene config. The Nyquist invariant (sample_rate_hz >= 2 * bandwidth_hz) plus the SionnaRT typed-geometry invariants are enforced at validation time, before any compute runs.

Fields

Field

Type

Required

Default

Description

sample_rate_hz

float (> 0)

no

20_000_000.0

Sample rate of the synthesized IQ tensor in Hz.

duration_s

float (> 0)

no

0.020

Duration of each scene in seconds. Output samples = int(round(sample_rate_hz * duration_s)); exact .5 ties follow Python round() and go to the nearest even integer.

bandwidth_hz

float (> 0)

no

10_000_000.0

Total occupied bandwidth. Must satisfy sample_rate_hz >= 2 * bandwidth_hz (Nyquist).

center_hz

float

no

0.0

Center frequency in Hz; 0.0 means baseband.

density

DensityConfig

no

DensityConfig()

Per-scene emitter-count distribution.

time_placement

TimePlacementStrategy

no

IID_UNIFORM

Time-domain placement strategy (closed enum). Event-timed emitters (radar PRI jitter, frequency-hop schedules, beacon cadence) are requested by selecting an EVENT_* member here plus time_placement_params; there is no separate event-overlay config.

time_placement_params

dict[str, object]

no

{}

Free-form per-strategy kwargs forwarded to the time placement strategy constructor.

frequency_placement

FrequencyPlacementStrategy

no

IID_UNIFORM

Carrier-frequency placement strategy (closed enum).

frequency_placement_params

dict[str, object]

no

{}

Free-form per-strategy kwargs forwarded to the frequency placement strategy constructor.

rx_array

RxArrayConfig

no

RxArrayConfig()

Receiver-array preset (used when multi_rx is None).

multi_rx

MultiRXConfig | None

no

None

Optional explicit multi-RX layout (geometry XOR receivers list).

channel_application

ChannelApplicationMode

no

SCENE

Whether channels apply per-scene or per-emitter.

geometry

SceneGeometryConfig

no

SceneGeometryConfig()

Scene-geometry backend selection plus overlap policy.

assets

SceneAssetsConfig

no

SceneAssetsConfig()

Scene-asset pointers. scene_geometry_ref is the preferred typed ref; the legacy *_uri fields are still accepted and are threaded into RT provenance as compatibility shims.

Validators

  • Nyquist. @model_validator(mode="after") enforces sample_rate_hz >= 2 * bandwidth_hz. Violations raise ValidationError with a “Nyquist requires at least 2x oversampling” message.

  • SionnaRT requires assets. A second @model_validator(mode="after") enforces that geometry.backend == SceneGeometryBackend.SIONNA_RT implies either assets.scene_geometry_ref or assets.scene_geometry_uri is set. The validator synthesises a ValidationError with loc=("assets", "scene_geometry_uri") so the error path names the offending leaf rather than the root. Other backends (NONE, MITSUBA) do not require scene-geometry assets.

  • SionnaRT validates typed geometry inputs after normalization. A companion @model_validator(mode="after") rejects two RT-only misconfigurations before the composer runs: hidden TX-pose surrogates under time_placement_params or frequency_placement_params (loc=("tx_pose",)), and assets.scene_geometry_ref values whose kind is not a scene asset (loc=("assets", "scene_geometry_ref", "kind")). For explicit multi_rx.receivers[i] entries, ordinary YAML that omits rx_pose is normalized first by ReceiverConfig from legacy position_m and quaternion orientation; the RT validator only sees rx_pose is None if a caller bypasses or defeats that normalization.

Example

from rfgen.config import SceneConfig

cfg = SceneConfig.model_validate({
    "sample_rate_hz": 20_000_000,
    "duration_s": 0.020,
    "bandwidth_hz": 10_000_000,
    "density": {"mode": "uniform", "min_emitters": 1, "max_emitters": 10},
    "rx_array": {"num_rx": 4, "array": "ula_4", "spacing_lambda": 0.5},
})
assert cfg.sample_rate_hz == 20_000_000
assert cfg.density.max_emitters == 10

class rfgen.config.DensityConfig

class DensityConfig(BaseModel):
    mode: DensityMode = DensityMode.RANGE
    min_emitters: int = Field(ge=0, default=1)
    max_emitters: int = Field(ge=1, default=10)
    poisson_rate: float | None = None  # mean emitter count, used directly (not per-MHz); required when mode is DensityMode.POISSON

Per-scene emitter-count distribution. A @model_validator(mode="after") enforces max_emitters >= min_emitters and requires poisson_rate when mode is DensityMode.POISSON.

Fields

Field

Type

Required

Default

Description

mode

DensityMode

no

RANGE

Distribution for the per-scene emitter count.

min_emitters

int (≥ 0)

no

1

Lower bound on emitter count (inclusive).

max_emitters

int (≥ 1)

no

10

Upper bound on emitter count (inclusive); must be ≥ min_emitters.

poisson_rate

float | None

no

None

Mean emitter count per scene draw, used directly as the mean of the Poisson distribution (before clipping to [min_emitters, max_emitters]). It is not a per-MHz density and is independent of scene bandwidth. Required when mode == POISSON.


class rfgen.config.RxArrayConfig

class RxArrayConfig(BaseModel):
    num_rx: int = Field(ge=1, default=1)
    array: str = "single"
    spacing_lambda: float = 0.5

Receiver-array geometry preset. Used when MultiRXConfig does not declare an explicit receivers list. The two are mutually exclusive; see MultiRXConfig.

array is open-string (not a closed enum) at this layer because concrete array presets register through the receiver-frontend module; the closed-set geometry tags live in MultiRXConfig.geometry.

The selectors the composer resolves today (case-insensitive, leading/trailing whitespace stripped) are:

array value

Meaning

num_rx agreement

single

Single receiver (default).

Requires num_rx == 1; any other value raises SceneError.

ula / linear

Uniform linear array; count comes from num_rx.

Uses num_rx directly.

ula_<N>

Uniform linear array of N elements (e.g. ula_8).

num_rx must be 1 (unset) or exactly N; a mismatch raises SceneError. N must be >= 1.

ura / uniform_rectangular

Parsed but always raises SceneError today: the shipped config carries no rows/cols fields.

n/a

arbitrary

Parsed but always raises SceneError today: the shipped config carries no positions_m field.

n/a

Any other value raises SceneError (“unknown rx_array.array selector”). Note that ura/arbitrary are accepted by the selector parse but rejected one step later when the geometry is expanded, because the shipped config surface has no fields to describe them; use an explicit MultiRXConfig.receivers list for those layouts. This selector path only applies when multi_rx.geometry is absent.


class rfgen.config.ReceiverConfig

class ReceiverConfig(BaseModel):
    rx_id: str = Field(min_length=1)
    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

One explicit receiver in MultiRXConfig.receivers. Use ReceiverConfig when the geometry presets are not expressive enough (mixed arrays, distributed sensing, asymmetric noise figures).

Validators. A @model_validator(mode="after") auto-derives rx_pose: when the caller omits rx_pose (the common case for ordinary YAML), it is back-filled from the legacy position_m and quaternion orientation fields (the quaternion is converted to Euler angles). Passing an explicit typed rx_pose: GeometryPoseConfig bypasses that legacy quaternion normalization and uses the given pose directly.

Receiver label threading. rx_id is the stable configured receiver label. The scene composer copies it into ChannelRxParams.tag for the active receiver on every channel call, and in multi-RX scenes mirrors the full configured receiver list into SceneMetadata.extras["receivers"][i]["rx_id"] for record-side lookup.


class rfgen.config.MultiRXConfig

class MultiRXConfig(BaseModel):
    geometry: ArrayGeometry | None = None
    receivers: list[ReceiverConfig] = Field(default_factory=list)

Multi-receiver layout: either a closed-enum geometry preset OR an explicit list of ReceiverConfig entries. The two are mutually exclusive. A @model_validator(mode="after") raises ValidationError when geometry is not None AND receivers is non-empty. This is one of the seven enumerated invalid combinations.

Fields

Field

Type

Required

Default

Description

geometry

ArrayGeometry | None

no

None

Closed-enum array preset (e.g. ULA, URA, ARBITRARY). Mutually exclusive with a non-empty receivers list.

receivers

list[ReceiverConfig]

no

[]

Explicit per-receiver list. Mutually exclusive with geometry.


class rfgen.config.SceneAssetsConfig

class SceneAssetsConfig(BaseModel):
    scene_geometry_uri: str | None = None
    scene_geometry_ref: GeometryAssetRef | None = None
    material_db_uri: str | None = None
    material_db_ref: GeometryAssetRef | None = None
    antenna_pattern_uri: str | None = None
    antenna_pattern_refs: tuple[GeometryAssetRef, ...] = ()

Scene-asset pointers (Mitsuba scene blob, material database, antenna patterns). The typed *_ref fields are the preferred content-addressed surface. The legacy *_uri fields remain valid and are converted by the scene composer into compatibility GeometryAssetRef entries so RT provenance, ChannelContext.geometry_asset_refs, and scene-level geometry hashes remain populated end to end. For URI-only configs the composer synthesizes deterministic compatibility hashes from (asset kind, normalized URI), so SceneMetadata.scene_geometry_hash and sibling *_hash fields are populated even without typed refs. Typed refs remain the authoritative surface when you need a caller-supplied blob hash.

When typed refs are present, their kind is validated at config time: material_db_ref.kind must be MATERIAL_DB, and every antenna_pattern_refs[*].kind must be ANTENNA_PATTERN.

All fields are optional at this schema. The SionnaRT-requires-assets validator on SceneConfig raises with loc=("assets", "scene_geometry_uri") when SceneConfig.geometry.backend == SceneGeometryBackend.SIONNA_RT and both scene_geometry_uri and scene_geometry_ref are None.


class rfgen.config.SceneGeometryConfig

class SceneGeometryConfig(BaseModel):
    backend: SceneGeometryBackend = SceneGeometryBackend.NONE
    overlap_policy: SceneOverlapPolicy = SceneOverlapPolicy.ALLOW
    rt_solver: RTSolverConfig | StatisticalSolverConfig | None = None

Scene-geometry backend selection. backend is a closed enum with three members:

  • SceneGeometryBackend.NONE: no geometry; flat-fading or analytical channels.

  • SceneGeometryBackend.SIONNA_RT: Sionna ray-traced geometry. Requires SceneConfig.assets.scene_geometry_ref or the legacy scene_geometry_uri. The pivot validator on SceneConfig enforces this.

  • SceneGeometryBackend.MITSUBA: Mitsuba scene blob. When scene assets are supplied, the composer threads them into SceneMetadata.geometry_asset_refs and the sibling *_hash audit fields even without RT propagation.

overlap_policy controls how the scene composer handles emitters whose time-frequency rectangles overlap; see SceneOverlapPolicy. rt_solver carries the optional solver knobs forwarded through ChannelContext.rt_solver_params: use RTSolverConfig for SceneGeometryBackend.SIONNA_RT, or StatisticalSolverConfig when a statistical Sionna backend consumes the same field.


class rfgen.config.GeometryPoseConfig

class GeometryPoseConfig(BaseModel):
    position_m: tuple[float, float, float]
    orientation_rad: tuple[float, float, float]
    velocity_mps: tuple[float, float, float] = (0.0, 0.0, 0.0)
    frame: Literal["scene"] = "scene"

Pydantic mirror of rfgen.core_types.GeometryPose, the frozen dataclass SionnaRT actually consumes. Used for SceneConfig.tx_pose and ReceiverConfig.rx_pose. orientation_rad is Euler angles (roll, pitch, yaw) in the scene frame; velocity_mps feeds SionnaRT’s Doppler wiring (a node with nonzero velocity produces genuinely time-varying channel taps when RTSolverConfig.cir_num_time_steps > 1, otherwise it has no effect). frame is fixed to "scene" for v1; no other coordinate frame is supported yet.


class rfgen.config.RTSolverConfig

class RTSolverConfig(BaseModel):
    max_depth: int = 3
    los: bool = True
    specular_reflection: bool = True
    diffuse_reflection: bool = False
    refraction: bool = True
    synthetic_array: bool = True
    scene_frequency_hz: float | None = None
    tx_array: ArraySpec = ArraySpec()
    rx_array: ArraySpec = ArraySpec()
    cir_l_min: int | None = None
    cir_l_max: int | None = None
    normalize_delays: bool = False
    cir_num_time_steps: int | None = None
    cir_sampling_frequency_hz: float | None = None
    allow_no_path: bool = False

Sionna RT solver knobs, forwarded verbatim into Sionna’s real PathSolver call (max_depth, los, specular_reflection, diffuse_reflection, refraction, synthetic_array) and scene setup. scene_frequency_hz sets scene.frequency directly; when unset, SionnaRT falls back to metadata.realized_carrier_hz, which requires SceneConfig.center_hz to be a positive absolute carrier (a baseband scene has no physical frequency to ray-trace at). tx_array/rx_array are typed ArraySpec sub-models forwarded to Sionna’s PlanarArray constructor (num_rows, num_cols, pattern, polarization); a typo’d key or an out-of-range pattern/polarization value fails at config-construction time with a Pydantic ValidationError naming the offending field, rather than leaking a raw TypeError from Sionna at solve time. More than one antenna element on either array raises ChannelError, since rfgen’s Signal type has no per-antenna representation. allow_no_path controls whether a zero-path solve raises ChannelError (False, the default) or returns all-zero IQ (True).


class rfgen.config.ArraySpec

class ArraySpec(BaseModel):
    num_rows: int = 1
    num_cols: int = 1
    pattern: Literal["iso", "dipole", "hw_dipole", "tr38901", "custom"] = "iso"
    polarization: Literal["V", "H", "VH", "cross"] = "V"
    id: str | None = None

Typed antenna-array spec for RTSolverConfig’s tx_array/rx_array. The permitted polarization set, and three of the four pattern members (iso/dipole/hw_dipole/tr38901), map onto Sionna’s registered PlanarArray pattern/polarization names. Sionna’s own PlanarArray constructor requires pattern and polarization on every call with no library-side default; the pattern="iso" / polarization="V" defaults here are rfgen-chosen conveniences (a single isotropic, vertically-polarized element, the simplest non-directional single-antenna array), not inherited Sionna defaults. extra="forbid", so an unknown key (e.g. patern) or an out-of-range pattern/polarization value raises a Pydantic ValidationError at construction.

pattern="custom" is rfgen’s own escape hatch, not a name Sionna itself registers: it requires an ANTENNA_PATTERN GeometryAssetRef on ctx.geometry_asset_refs, whose entrypoint must be a .py file defining a module-level v_pattern(theta, phi) -> mi.Complex2f function, the same vertically-polarized pattern-function contract Sionna’s own built-in patterns use internally. SionnaRT loads that function, wraps it in Sionna’s own PolarizedAntennaPattern (unchanged polarization/model handling), and registers it with Sionna under a content-hash-derived name before building the array.

The array-identity string recorded in GeometryProvenance prefers an explicit id when set, else synthesizes planar-<num_rows>x<num_cols>-<pattern>-<polarization>. Both SionnaRT and the scene composer’s pre-solve provenance stand-in derive that identity from this one model, so the solved and stand-in provenance never disagree.


class rfgen.config.PanelArraySpec

class PanelArraySpec(BaseModel):
    num_rows_per_panel: int = 1
    num_cols_per_panel: int = 1
    polarization: Literal["single", "dual"] = "single"
    polarization_type: Literal["V", "H", "VH", "cross"] = "V"
    antenna_pattern: Literal["omni", "38.901"] = "omni"

Typed antenna-array spec for the three system-level statistical propagation backends (SionnaUMa, SionnaUMi, SionnaRMa) and for SionnaCDL. These construct Sionna’s sionna.phy.channel.tr38901.PanelArray, a different type from ArraySpec’s target (sionna.rt.PlanarArray) with a different keyword surface, hence a separate model. Sionna’s own constraint, enforced here at config-construction time (fails fast with a Pydantic ValidationError rather than Sionna’s own deep-in-construction AssertionError), is that polarization="single" requires polarization_type to be "V" or "H", while polarization="dual" requires "VH" or "cross".


class rfgen.config.StatisticalSolverConfig

class StatisticalSolverConfig(BaseModel):
    carrier_frequency_hz: float | None = None
    direction: Literal["uplink", "downlink"] = "downlink"
    o2i_model: Literal["low", "high"] = "low"
    enable_pathloss: bool = True
    enable_shadow_fading: bool = True
    ut_array: PanelArraySpec = PanelArraySpec()
    bs_array: PanelArraySpec = PanelArraySpec()
    model: Literal["A", "B", "C", "D", "E"] = "A"
    delay_spread_s: float = 100e-9
    min_speed_mps: float = 0.0
    max_speed_mps: float | None = None
    cir_l_min: int | None = None
    cir_l_max: int | None = None
    cir_num_time_steps: int | None = None
    cir_sampling_frequency_hz: float | None = None

Solver knobs for the five statistical (non-ray-traced) Sionna TR 38.901 propagation backends: SionnaUMa, SionnaUMi, SionnaRMa (system-level, topology derived from ctx.tx_pose/ctx.rx_params.rx_pose) and SionnaTDL, SionnaCDL (link-level, no topology at all). One config surface shared across both families, mirroring how RTSolverConfig is one config surface for ray-tracing knobs not every RT call path exercises; each backend reads only the subset of fields its underlying Sionna model accepts (e.g. o2i_model is ignored by RMa, TDL/CDL, and ut_array/bs_array are ignored by SionnaTDL). carrier_frequency_hz falls back to metadata.realized_carrier_hz when unset, same as RTSolverConfig.scene_frequency_hz. model is a 3GPP scenario letter ("A"-"E") consumed only by SionnaTDL/SionnaCDL – Sionna names both model families’ scenario letters the same way, but TDL-A…E and CDL-A…E are distinct physical scenarios. cir_l_min/cir_l_max/ cir_num_time_steps/cir_sampling_frequency_hz are the same discrete-time channel conversion knobs as RTSolverConfig’s fields of the same name.


class rfgen.config.PlacementConfig

class PlacementConfig(BaseModel):
    time_strategy: TimePlacementStrategy = TimePlacementStrategy.IID_UNIFORM
    freq_strategy: FrequencyPlacementStrategy = FrequencyPlacementStrategy.IID_UNIFORM
    grid_source: str = "json_manifest"   # open: rfgen.grid_sources entry-point name
    channel_plan_source: str = "json_manifest"   # compatibility alias for grid_source

Selects the canonical time and frequency placement strategies plus the BaseGridSource plug-in used by the realistic_density frequency placement strategy to look up per-band channel grids. time_strategy and freq_strategy are closed enum fields; YAML uses their enum values. grid_source is an open-set string resolved through the rfgen.grid_sources entry-point group at strategy instantiation. The default "json_manifest" value resolves to JsonManifestGridSource, which loads per-band JSON files from rfgen/placement/data/grids/<band>.json. channel_plan_source remains as a compatibility alias and must match grid_source when both are provided.

Third-party BaseGridSource plugins register under rfgen.grid_sources without touching this schema or the strategy implementation.

Fields

Field

Type

Required

Default

Description

time_strategy

TimePlacementStrategy

no

IID_UNIFORM

Canonical time-domain placement strategy selector.

freq_strategy

FrequencyPlacementStrategy

no

IID_UNIFORM

Canonical frequency-domain placement strategy selector.

grid_source

str

no

"json_manifest"

Open-set entry-point name resolved through rfgen.grid_sources.

channel_plan_source

str

no

"json_manifest"

Compatibility alias for grid_source; both values must match when both are provided.


class rfgen.config.EmitterZooConfig

class EmitterZooConfig(BaseModel):
    families: list[EmitterFamilyConfig] = Field(default_factory=list)

Pool of emitters with per-family sampling weights. The default is an empty list so a no-override GenerationConfig() build surfaces only the two genuinely required missing fields (run.run_id and storage.path). The “families must be non-empty” runtime check is deferred to the scene composer (Layer 4); the composer raises a SceneError when it tries to draw from an empty pool, with a message pointing at this config block.


class rfgen.config.EmitterFamilyConfig

class EmitterFamilyConfig(BaseModel):
    family: EmitterFamily
    classes: list[str] = Field(min_length=1)
    weight: float = Field(gt=0.0, default=1.0)
    params: dict[str, object] = Field(default_factory=dict)
    fingerprint: FingerprintConfig | None = None

Per-family entry in EmitterZooConfig.families. Names a family (closed enum), restricts it to a non-empty subset of supported_classes, sets a relative sampling weight, forwards params to BaseEmitter.generate, and optionally attaches a FingerprintConfig.

Fields

Field

Type

Required

Default

Description

family

EmitterFamily

yes

Top-level emitter family (closed enum).

classes

list[str] (min_length=1)

yes

Subset of BaseEmitter.supported_classes; non-empty.

weight

float (> 0)

no

1.0

Relative sampling weight; normalized across all entries.

params

dict[str, object]

no

{}

Forwarded to BaseEmitter.generate(..., params=...).

fingerprint

FingerprintConfig | None

no

None

Per-device impairment priors.


class rfgen.config.FingerprintConfig

class FingerprintConfig(BaseModel):
    enabled: bool = True
    num_devices: int = Field(ge=1, default=10)
    cfo_hz_range: tuple[float, float] = (-1000.0, 1000.0)
    sfo_ppm_range: tuple[float, float] = (-20.0, 20.0)
    iq_imbalance_db_range: tuple[float, float] = (-1.0, 1.0)
    phase_noise_dbc_hz_range: tuple[float, float] = (-110.0, -90.0)
    pa_model: PAModel = PAModel.RAPP

Prior over per-device CFO / SFO / IQ-imbalance / phase-noise / PA-model parameters. Consumed by the device-fingerprint module. See Fingerprint Math.

pa_model is a closed PAModel enum. Unknown values fail during Pydantic schema validation.


class rfgen.config.ChannelConfig

class ChannelConfig(BaseModel):
    name: str = "torchsig_impairments"             # legacy compatibility field
    chain: list[ChannelChainEntry] = Field(default_factory=list)
    params: dict[str, object] = Field(default_factory=dict)
    snr_db_range: tuple[float, float] = (-10.0, 30.0)

Channel chain selection. The live runtime surface is chain, an ordered list of ChannelChainEntry items, each naming a closed-enum Transformation. The top-level name, params, and snr_db_range fields remain in the schema only as legacy compatibility placeholders; the shipped validators require the default name, empty params, and the default snr_db_range so user config cannot silently set values that runtime ignores.

A @model_validator(mode="after") enforces three cross-entry invariants. All three are part of the seven enumerated invalid combinations:

  1. At most one Group.CHANNEL entry. More than one entry whose transformation.group == Group.CHANNEL raises ValidationError. The channel-propagation slot is single-instance per chain.

  2. Group order is monotonic non-decreasing across the full chain. A later entry may move from TX to CHANNEL to RX, but it may not backtrack from a later group into an earlier one.

  3. Adjacent same-group ordinals are monotonic non-decreasing. Two adjacent entries in the same group whose transformation ordinals decrease raise ValidationError. This catches a common config-time bug where a TX impairment is inserted out of order after a later one.

Fields

Field

Type

Required

Default

Description

name

str

no

"torchsig_impairments"

Legacy compatibility field. Must stay at the default value; live channel selection uses chain.

chain

list[ChannelChainEntry]

no

[]

Ordered transformation list.

params

dict[str, object]

no

{}

Legacy compatibility field. Must stay empty; live per-transformation params belong on chain[].params.

snr_db_range

tuple[float, float]

no

(-10.0, 30.0)

Legacy compatibility field. Must stay at the default value; runtime does not consume it.


class rfgen.config.ChannelChainEntry

class ChannelChainEntry(BaseModel):
    transformation: Transformation                 # closed enum
    params: dict[str, object] = Field(default_factory=dict)

One transformation entry in ChannelConfig.chain. The transformation field is a closed Transformation enum member; params is validated against the concrete channel’s own schema() at instantiation, not at config validation.


class rfgen.config.StorageConfig

class StorageConfig(BaseModel):
    backend: StorageBackend | str = StorageBackend.ZARR_LOCAL
    path: str = Field(min_length=1)                # required, no default
    compression: str = "blosc"
    chunk_samples: int = Field(gt=0, default=128)
    record_axis: RecordAxis = RecordAxis.PER_RX
    assets_path: str | None = None

Selects the BaseStore backend and configures on-disk layout. path has no default and is one of the two no-default leaves driving the no-override-build error contract on GenerationConfig.

SIGMF path-scheme validator

A @model_validator(mode="after") enforces that backend == StorageBackend.SIGMF implies path starts with one of:

  • file://

Any other prefix raises ValidationError. This is one of the seven enumerated invalid combinations. Other backends do not enforce a URI scheme at this layer; per-backend validation happens at store construction.

Fields

Field

Type

Required

Default

Description

backend

StorageBackend | str

no

ZARR_LOCAL

Built-in backend enum or a custom rfgen.stores plugin name string.

path

str (min_length=1)

yes

Output URI; required. The contract test for GenerationConfig() asserts the raised ValidationError names this leaf.

compression

str

no

"blosc"

Compression codec. Shipped built-ins validate it as Zarr: none/blosc, HDF5: none/gzip/lzf, WebDataset: none/gzip/bz2/xz, SigMF: none. Effective defaults rewrite to gzip for HDF5 and none for WebDataset and SigMF when the caller leaves the field at "blosc".

chunk_samples

int (> 0)

no

128

IQ chunk length in samples for Zarr and HDF5 only. Explicitly setting it for WebDataset or SigMF raises ValidationError.

record_axis

RecordAxis

no

PER_RX

Multi-RX record layout.

assets_path

str | None

no

None

Path to the content-addressed asset store.


class rfgen.config.ExecutorConfig

class ExecutorConfig(BaseModel):
    name: str = "local"                            # open-set plugin selector
    parallelism: int = Field(default=1, gt=0)

Selects and parameterizes the BaseDistributedExecutor. name is open-set (executors register through the rfgen.executors entry-point group). New backends do not require updating this schema. parallelism is the requested PySpark partition count. Synchronous local execution ignores it. The value must be a positive integer, and booleans are rejected before Pydantic’s integer coercion. Unknown keys, including the removed free-form params mapping, fail validation.


class rfgen.config.LabelConfig

class LabelConfig(BaseModel):
    name: str = "joint"
    params: dict[str, object] = Field(default_factory=dict)
    extra_labelers: list[str | LabelerSpec] = Field(default_factory=list)
    seg_n_fft: int = Field(gt=0, default=1024)
    seg_hop: int = Field(gt=0, default=256)
    segmentation_mode: SegmentationMode = SegmentationMode.SINGLE_LABEL
    segmentation_tie_break: SegmentationTieBreak = SegmentationTieBreak.LOWER_EMITTER_INDEX

Selects the primary labeler, additional labeler plugins, and per-task label parameters. STFT parameters (seg_n_fft, seg_hop) define the time-frequency grid for segmentation masks. segmentation_mode selects single-label vs multi-label masks, and segmentation_tie_break documents the single-label overlap rule. Segmentation capability is selected by the active labeler itself: bbox stays metadata-only, while segmentation and joint emit segmentation by construction. See Label Schema.


class rfgen.config.LabelerSpec

class LabelerSpec(BaseModel):
    name: str = Field(min_length=1)
    params: dict[str, object] = Field(default_factory=dict)

One entry in LabelConfig.extra_labelers. Either a plain registry-name string or a LabelerSpec with per-labeler params.


class rfgen.config.AnnotatorConfig

class AnnotatorConfig(BaseModel):
    enabled: StrictBool = True
    types: list[AnnotationType] = Field(default_factory=lambda: [AnnotationType.CAPTION])
    bulk_llm: LLMConfig                            # required
    verifier_llm: LLMConfig | None = None
    verifier_subset_pct: float = Field(ge=0.0, le=100.0, default=0.0)

Inference templating layer config. bulk_llm is required; verifier_llm is optional unless verifier_subset_pct is greater than 0.0. The shipped config accepts percentages from 0.0 through 100.0. Intermediate verifier subsets must be paired with preselected membership from select_paes_verifier_subset(...) before records are handed to the annotator. When the parent slot is None on GenerationConfig, the annotation phase is skipped and these fields never participate in validation. When the block is present and enabled is False, config validation still runs for the block, but the runtime annotation phase is a no-op and clients are not constructed. enabled uses Pydantic StrictBool: only YAML/JSON booleans (true/false) are valid. Numeric and string-like booleans such as 0, 1, "false", and "true" are rejected at config-validation time.

The field names bulk_llm and verifier_llm are retained from the config-surface naming; the configured endpoints register through the rfgen.inference_clients entry-point group and are not restricted to text-only LLMs.


class rfgen.config.LLMConfig

class LLMConfig(BaseModel):
    provider: str                                  # open-set plugin selector
    model: str
    temperature: float = Field(ge=0.0, le=2.0, default=0.2)
    max_tokens: int = Field(gt=0, default=512)
    json_schema_mode: StrictBool = True

One inference endpoint configuration. Used twice inside AnnotatorConfig: once as bulk_llm (high-volume, low-cost) and optionally once as verifier_llm (second-pass hallucination check). provider is open-set; inference clients register through the rfgen.inference_clients entry-point group. json_schema_mode requests structured output where the provider supports it. It uses Pydantic StrictBool: only YAML/JSON booleans (true/false) are valid. Numeric and string-like booleans such as 0, 1, "false", and "true" are rejected at config-validation time.

The class name LLMConfig and the field names bulk_llm / verifier_llm are retained for config-surface stability; the configured providers can be text-only LLMs, vision-language models, or audio-language models.


class rfgen.config.AnnotationOrchestratorConfig

class AnnotationOrchestratorConfig(BaseModel):
    name: str = "local_loop"                       # open-set plugin selector
    min_poll_interval_s: float = Field(default=10.0, ge=0.0)
    resources: BatchResources | None = None

Selects and parameterizes the batch annotation orchestrator (e.g. vertex_batch, anthropic_batch, openai_batch, local_loop). name is open-set; orchestrators register through the rfgen.annotation_orchestrators entry-point group.

The Phase-2 annotate command consumes this schema separately from GenerationConfig. Each managed orchestrator requires its matching typed resource model. local_loop rejects managed resources. The removed bulk_model, verifier_model, and params fields are unknown keys and fail validation.

Managed execution is resumable and uses at-least-once result delivery keyed by the stable annotation-job identity (sample_id, annotation_type, template_id, run_id). The command checkpoints normalized results before storage writes, records each durable key, and cleans up provider staging only after every result is durable. Retrying a persisted handle skips completed keys; retrying after a cleanup failure repeats cleanup without refetching or rewriting durable annotations. Configure provider staging and retention through resources; the schema has no alternate retry or idempotency-key field.

Fields

Field

Type

Required

Default

Description

name

str

no

"local_loop"

Open-set plugin name.

min_poll_interval_s

float (>= 0)

no

10.0

Minimum seconds between provider status requests. Booleans and negative values fail validation.

resources

BatchResources | None

no

None

Provider-specific staging and model resources. Required for managed orchestrators.

Typed batch resources

All resource models forbid unknown keys, strip surrounding whitespace from strings, and reject blank strings. URI validators require a non-empty authority and the provider-specific scheme.

Resource model

Orchestrator

Required fields

Optional controls

OpenAIBatchResources

openai_batch

model

endpoint (/v1/responses or /v1/chat/completions), completion_window="24h", delete_input_file_after_fetch=True

AnthropicBatchResources

anthropic_batch

model

max_tokens=1024 (> 0)

VertexBatchResources

vertex_batch

project, location, model, input_uri, output_uri

retain_staging=False; both URIs use gs://

SageMakerBatchResources

sagemaker_batch

model_name, input_s3_uri, output_s3_uri

instance_type="ml.m5.large", instance_count=1 (> 0), retain_staging=False; both URIs use s3://

AzureBatchResources

azure_batch

subscription_id, resource_group, workspace_name, storage_account_url, endpoint_name, deployment_name, input_uri, output_uri

retain_staging=False; input and output URIs use azure://

BatchResources is the union of these five models. Provider validation is exact: for example, name="vertex_batch" accepts only VertexBatchResources, not another resource model with similar fields.


class rfgen.config.AuditConfig

class AuditConfig(BaseModel):
    snr_mean_error_db: float = Field(default=0.5, ge=0.0, le=100.0)
    class_kl_divergence: float = Field(default=0.05, ge=0.0, le=1.0)
    overlap_error: float = Field(default=0.02, ge=0.0, le=1.0)
    occupancy_error: float = Field(default=0.05, ge=0.0, le=1.0)
    max_records: int = Field(default=1_000_000, gt=0)
    max_class_cardinality: int = Field(default=10_000, gt=0)
    max_snr_bucket_cardinality: int = Field(default=10_000, gt=0)
    max_emitter_count_cardinality: int = Field(default=10_000, gt=0)

Controls dataset-audit failure thresholds and bounds every aggregation whose size depends on dataset metadata. Threshold fields reject booleans and enforce their documented ranges. Record and cardinality bounds must be positive integers and also reject booleans.

Field

Default

Contract

snr_mean_error_db

0.5

Maximum absolute realized-versus-requested mean SNR error in dB; range [0, 100].

class_kl_divergence

0.05

Maximum class-distribution KL divergence; range [0, 1].

overlap_error

0.02

Maximum absolute cochannel-overlap error; range [0, 1].

occupancy_error

0.05

Maximum absolute spectral-occupancy error; range [0, 1].

max_records

1_000_000

Maximum records consumed by one audit.

max_class_cardinality

10_000

Maximum realized or requested class keys retained by audit histograms.

max_snr_bucket_cardinality

10_000

Maximum requested-SNR buckets retained by the audit.

max_emitter_count_cardinality

10_000

Maximum distinct realized emitter counts retained by the audit.


class rfgen.config.CredentialsConfig

class CredentialsConfig(BaseModel):
    provider: str = "static"
    params: dict[str, object] = Field(default_factory=dict)

Selects the BaseCredentialsProvider. Usually omitted; cloud SDKs fall back to default credential chains. The config exists so a user can pin a non-default resolution path. Documented provider values: "static", "gcp_adc", "aws_default", "azure_default", plus any third-party provider registered under rfgen.credentials.


class rfgen.config.ReferenceHostConfig

class ReferenceHostConfig(BaseModel):
    cpu_model: str = Field(min_length=1)
    cpu_cores: int = Field(ge=1)
    ram_gb: int = Field(ge=1)
    disk_type: Literal["nvme", "ssd", "hdd"]
    os: str = Field(min_length=1)
    python_version: str = Field(min_length=1)
    torch_version: str = Field(min_length=1)

Reference-host metadata used by per-test-suite YAML fixtures. The schema lives at rfgen.config.reference_host so Layer 7 (dataset-consumer) and Layer 9 (cli, validation-and-audit) consumers import it from below without a forward-layer edge. The fixture YAML files (tests/conf/dataloader_reference_host.yaml, tests/conf/cli_reference_host.yaml) are authored under validation-and-audit; only the schema lives here.

disk_type is Literal["nvme", "ssd", "hdd"] rather than a closed enum because the value is a fixture-only tag with no runtime selection logic hanging off it; the closed-set rule applies to fields that drive code paths.

Method: matches_runtime

def matches_runtime(self) -> bool:
    ...

Returns True iff the configured host matches the test runner’s platform.python_version(), torch.__version__, plus a CPU model probe. Tests use the result to decide between running and xfail.

The CPU probe uses platform.processor(). When that returns the empty string (the default on Linux), matches_runtime() returns False rather than silently passing the substring check. This is the explicit choice: a non-match on an unknown host is safer than a silent pass that lets a wall-clock or cold-import contract test report success on the wrong CPU.


Enumerated invalid combinations

Every config that fails one of the cross-field invariants below raises pydantic.ValidationError. The table below lists the documented cross-field invariants enforced by the shipped config models. Plugin- or backend-specific failures surface later, at instantiation time.

#

Failure

Where enforced

1

SceneConfig.sample_rate_hz < 2 * SceneConfig.bandwidth_hz

SceneConfig._check_grid (@model_validator(mode="after"))

2

Any of sample_rate_hz, bandwidth_hz, duration_s ≤ 0

Field constraints (Field(gt=0)) on SceneConfig

3

More than one entry in ChannelConfig.chain whose transformation.group == Group.CHANNEL

ChannelConfig._check_chain

4

MultiRXConfig populated with both geometry and a non-empty receivers list

MultiRXConfig._geometry_xor_receivers

5

StorageConfig.backend == StorageBackend.SIGMF paired with path not starting with file://

StorageConfig._sigmf_path_scheme

6

ChannelConfig.chain with two adjacent same-Group entries whose ordinals are not monotonic non-decreasing

ChannelConfig._check_chain

7

SceneConfig.geometry.backend == SceneGeometryBackend.SIONNA_RT with both assets.scene_geometry_uri and assets.scene_geometry_ref unset

SceneConfig._sionna_rt_requires_assets (loc=("assets", "scene_geometry_uri"))

8

SceneConfig.geometry.backend == SceneGeometryBackend.SIONNA_RT with time_placement_params or frequency_placement_params hiding tx_pose, tx_position, or tx_orientation

SceneConfig._site_geometry_validates_asset_kind_and_pose (loc=("tx_pose",))

9

SceneConfig.geometry.backend == SceneGeometryBackend.SIONNA_RT with assets.scene_geometry_ref.kind outside the shipped scene-asset kinds

SceneConfig._site_geometry_validates_asset_kind_and_pose (loc=("assets", "scene_geometry_ref", "kind"))

10

SceneConfig.geometry.backend == SceneGeometryBackend.SIONNA_RT with an explicit multi_rx.receivers[i] entry whose rx_pose is still None after ReceiverConfig normalization, for example if a caller constructs models while bypassing validators

SceneConfig._site_geometry_validates_asset_kind_and_pose (loc=("multi_rx", "receivers", i, "rx_pose"))

from_hydra adds two transport-level failure modes that surface as ConfigError, not ValidationError: non-config / non-dict input (e.g. a ListConfig) and a top-level container that resolves to anything other than a dict. Both are documented under from_hydra().


Closed-set vs open-set field rule

Closed-set fields use Layer 1 StrEnums from rfgen.enums. Open-set plugin selectors stay typed str and resolve through the registry at instantiation.

Closed-set (StrEnum from rfgen.enums)

Open-set (str, registry-resolved)

DensityConfig.mode (DensityMode)

ExecutorConfig.name (rfgen.executors)

SceneConfig.time_placement (TimePlacementStrategy)

LabelConfig.name (rfgen.labels)

SceneConfig.frequency_placement (FrequencyPlacementStrategy)

LLMConfig.provider (rfgen.inference_clients)

SceneConfig.channel_application (ChannelApplicationMode)

CredentialsConfig.provider (rfgen.credentials)

SceneGeometryConfig.backend (SceneGeometryBackend)

LabelConfig.name (rfgen.labelers)

SceneGeometryConfig.overlap_policy (SceneOverlapPolicy)

LabelerSpec.name (rfgen.labelers)

MultiRXConfig.geometry (ArrayGeometry)

RxArrayConfig.array (open-string preset tag)

EmitterFamilyConfig.family (EmitterFamily)

StorageConfig.compression (codec set varies per backend)

AnnotatorConfig.types (list of AnnotationType)

PlacementConfig.grid_source (rfgen.grid_sources)

FingerprintConfig.pa_model (PAModel)

ChannelChainEntry.transformation (Transformation)

StorageConfig.record_axis (RecordAxis)

StorageConfig.backend (built-in StorageBackend values plus custom rfgen.stores plugin names)

YAML may use the enum string value directly because Pydantic deserializes it at validation time; Python code MUST use the enum member (see Style § Enum vs string in Python examples).


Retired channel config blocks

AWGNConfig and ReceiverFrontendConfig belonged to the previous five-stage channel design (one coarse RX_FRONTEND stage followed by a standalone AWGN stage). Both are retired by the semantic-group, fourteen-transformation pipeline documented in channels.md: thermal noise now enters as LNA_NOISE inside Group.RX_CAPTURE via BaseLNANoise, and the coarse RX frontend is split across BaseRXMixer, BaseIFFilter, BaseResampler, BaseLNANoise, BaseADCQuantization, BaseRXPhaseNoise, BaseRXIQImbalance, and BaseAGC. Per-transformation parameter blocks live with each ABC’s concrete backends; see ChannelConfig for the chain-level config that aggregates them.


See Also