rfgen.config

Pydantic v2 models validate one generation job before a sample is generated. Hydra composes YAML trees and CLI overrides into a mapping; rfgen.config turns that mapping into a typed GenerationConfig. A failed configuration does not start generation.

Entry points

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

config = from_hydra(OmegaConf.load("configs/config.yaml"))
assert isinstance(config, GenerationConfig)

from_hydra(cfg) resolves an OmegaConf mapping then validates it as a GenerationConfig. validate_config(data) validates a plain mapping. Both raise rfgen.core.errors.ConfigError with Pydantic error details when the input is invalid.

from_hydra accepts either an OmegaConf node or a plain dict. It resolves interpolations before validation and rejects a non-mapping top level with a ConfigError. validate_config is the matching boundary for an already materialized mapping. Direct GenerationConfig.model_validate(...) remains useful when a caller deliberately wants Pydantic’s native ValidationError.

Composition root

GenerationConfig composes these generic framework concerns:

Area

Primary model

Responsibility

Run

RunConfig

Run identifier, sample count, shard size, and seed.

Scene

SceneConfig

Signal grid, density, receiver geometry, and assets.

Scene plan

ScenePlanConfig

Authored per-run scene-plan template: clock, world asset URI, systems, event schedule with per-event communications blocks, and target sampling bounds.

Emitters

EmitterZooConfig, EmitterFamilyConfig, FingerprintConfig

Emitter pool and optional device-impairment priors.

Channel

ChannelConfig, ChannelChainEntry

Ordered signal transformations.

Labels

LabelConfig, LabelerSpec

Structured label production.

Storage

StorageConfig

Store backend and destination URI.

Execution

ExecutorConfig

Local or registered distributed executor selection.

Additional public models cover annotation, credentials, placement, and reference-host metadata. rfgen.config.__init__ re-exports these supported models, so from rfgen.config import GenerationConfig, RunConfig is the stable concise import path.

GenerationConfig

GenerationConfig is the single configuration object consumed by generic generation. Its constructor is equivalent to the following Pydantic surface: GenerationConfig(*, emitter_zoo=EmitterZooConfig(), channel=ChannelConfig(), scene=SceneConfig(), placement=PlacementConfig(), label=LabelConfig(), annotator=None, augmentation=None, storage=None, executor=ExecutorConfig(), credentials=None, run=None, projections=None, observation=ObservationConfig(), plan=None).

Field

Type

Required

Default

Primary behavior

emitter_zoo

EmitterZooConfig

no

empty zoo

Declares explicitly selected emitter families.

channel

ChannelConfig

no

empty chain

Declares ordered signal transformations.

scene

SceneConfig

no

default signal grid

Declares scene geometry, duration, density, and receivers.

placement

PlacementConfig

no

IID placement

Selects time/frequency placement strategies.

label

LabelConfig

no

joint

Selects structured label production.

annotator

AnnotatorConfig | None

no

None

Optional later annotation configuration.

augmentation

AugmentationConfig | None

no

None

Optional registered record augmentation.

storage

StorageConfig | None

no

None

Names the generic output backend and destination when an executor will persist records.

executor

ExecutorConfig

no

local

Selects local or registered execution.

credentials

CredentialsConfig | None

no

None

Optional local credential-provider configuration.

run

RunConfig | None

no

None

Supplies run identity, shard sizing, and deterministic seed for executor-backed generation.

projections

tuple[ObservationProjectionConfig, ...] | None

no

None

Nonempty ordered observation-projection instances, and mandatory: a run declares what it observes through this field and nothing else. IDs must be unique.

observation

ObservationConfig

no

1 GiB tensor budget

Bounds the complete aggregate Signal Dataset record.

plan

ScenePlanConfig | None

no

None

Optional scene-plan template (in rfgen.config.plan) minted into one ScenePlan per sample. Setting it makes the plan the single source of truth for what it covers, so every overlapping private parameter must be absent or unset.

ObservationProjectionConfig has four fields: a unique portable projection_id, a namespaced plugin selector, a positive strict contract_version, and a parameter mapping. Unified observation execution requires plan, projections, and a Signal Dataset store. The runtime resolves the complete set before it opens the store or renders a projection. See Observation API and Generate one unified observation dataset. Complete ScenePlan, projection, and observation field tables are in Observation Configuration.

The scene plan and its exclusivity rules

plan is a template, not a plan: a ScenePlan carries one sample’s identity and its concrete targets, while a configuration is per run and describes many samples. A configured literal would make every sample identical and make the second sample’s write-once provenance artifact collide on scene_id. rfgen.scene.minting.mint_scene_plan(template, run_seed=..., sample_index=...) mints one plan per sample, and sample_index is the run-global index, shard_index * shard_size + local_index, in both the local and the Dataproc driver.

A plan is the single source of truth for what it covers, and the two halves of that rule are enforced differently because they are different kinds of object.

The radar renderer’s own plan-owned fields are not fields at all. targets, system.location_m, system.rotation_deg, system.transmitter_element_locations_m, system.receiver.element_locations_m, and system.waveform.pulse_repetition_intervals_s were removed from RadarRendererParams, so extra="forbid" refuses them by name. A check that merely ignored them would leave an author’s value silently discarded.

The scene fields below are typed with defaults, so exclusivity is checked against model_fields_set: comparing to the default would silently accept an author who wrote the default, and reject an explicit value that agrees with the plan.

Scene field

Plan field that owns it

Check

scene.tx_pose

PlannedSystem.pose of a communications system

model_fields_set

scene.tx_power_dbm

per-event tx_power_dbm in the comms block

model_fields_set

scene.event_duration

PlannedEvent start and stop

model_fields_set

scene.density

plan.events

model_fields_set

The scene.* rows are checked for every projection, because those fields belong to the scene configuration every projection composes through.

One rule sits beside the table. plan.clock.duration_s must equal scene.duration_s. That is an agreement check rather than an exclusivity row: no plan fact routes into the communications record length, so the scene field stays authoritative and settable, and only a configuration whose two halves describe different scenes is refused.

One further rule governs how the world is named. plan.scene_geometry_uri is optional, and a template that leaves it unset plans against an empty world. When it is set, it must be an absolute filesystem path or open with one of the supported URI prefixes, which are exactly file://, gs://, s3://, https://, and sionna://builtin/. A relative path resolves against the working directory of whichever process mints the plan, and since the world’s bytes are digested into the plan hash and therefore into time_reference, a relative name would make the plan identity depend on where the run was launched from. A URI whose scheme is not on that list is refused by name (plan_world_asset_unsupported_scheme) rather than accepted, because nothing resolves it as a URI: s3a://bucket/world.xml would fall through to the same working-directory resolution a bare relative path takes, and being well formed does not make it locatable from a second machine. A relative path is refused under plan_world_asset_relative_path, and both errors list the supported prefixes.

A file:// URI carries a supported prefix and still has to name an absolute path, so file://world.xml is refused under plan_world_asset_relative_file_uri. Everything between file:// and the first slash is a URI authority, not a path: world.xml there is dropped when the reference is resolved, leaving the working directory itself. The two spellings that mean this machine, file:///path and file://localhost/path, are accepted; any other authority names a host nothing here fetches from. A local asset that cannot be read where minting runs is refused for the same family of reasons, naming the resolved path, rather than falling back to a URI-identity digest; a remote gs:// URI keeps that fallback, which is deterministic because it never consults readability at all.

The kind the world asset takes is derived from that URI, by the scheme-then-path-suffix table in GeometryAssetKind’s suffix rule. plan.scene_geometry_kind is the optional escape hatch for the one case a suffix genuinely cannot decide: a URI naming a content-addressed blob, an extensionless export, or a directory-style entrypoint. Leaving it unset is the ordinary case and means the suffix table decides alone; absence is what keeps every template written before the field existed on the kind it already had.

The field is accepted only where the suffix table falls through. On a URI the table classifies positively, a kind that agrees is accepted as a redundant restatement, and a kind that disagrees is refused under scene_geometry_kind_conflicts_with_suffix, naming the URI, the suffix-derived kind, and the declared kind. The refusal rather than a precedence rule is the same answer plan_world_asset_disagreement gives for a URI disagreement, and for the same reason: both facts are authored by one person in one file, so one of them is a typo and there is no reading under which silently preferring either is more likely to be what was meant. It also closes a cache collision that only the override can reach, since a local asset’s digest is sha256 of its bytes and does not mention the kind, so one file declared two ways carries one content_hash while naming two different ingest paths. An author who genuinely has a USD stage named .xml should rename the file rather than teach the configuration to disbelieve it.

Two further refusals: scene_geometry_kind_not_a_world for a kind that can never be world geometry (material_db, antenna_pattern), because accepting one would defer the failure to the engine; and scene_geometry_kind_without_uri when a kind is declared beside no scene_geometry_uri at all, because a kind without an asset describes nothing.

A fourth refusal is about the other route to the world. plan.scene_geometry_kind is resolved on the minting route and on no other: DefaultSceneComposer derives its kind from the bare scene.assets.scene_geometry_uri through the same suffix table, and it never sees the plan. So declaring a kind on the plan side while also naming a world on the scene side describes one asset under two kinds, and for a local asset both kinds arrive at the same digest and the same world-cache key, where whichever route loads first wins the entry. That pair is refused under scene_geometry_kind_beside_composer_world, naming both paths. The remedy is the typed scene.assets.scene_geometry_ref, which carries the kind and the digest as one authored reference the composer prefers over the legacy URI; note that under a geometry backend that loads a world the typed reference accepts only the kinds that backend can load, so a USD kind has no route to the scene side at all. That limit is scoped to the backend: under scene.geometry.backend: none the allowlist does not run and the kind is unrestricted, which is inert rather than a second route, because a scene with no geometry backend emits no geometry asset refs at all. The refusal message says whichever of those two things is true for the backend it read, and lists kinds only when there is a list.

An unknown spelling of the field is refused by the field itself, and the message lists the kinds worth naming there rather than every member of the enum: material_db and antenna_pattern are refused as non-world kinds, opengert_mitsuba_xml_bundle is unreachable through this field because an OpenGERT export is Mitsuba XML and its .xml suffix classifies it positively, and deepmimo_export names a format no shipped ingest reads.

scene.geometry.geometry_ingest

The engine ingest that turns world geometry into an engine world, named from the rfgen.geometry_ingests entry-point group. Leaving it unset is the ordinary case and resolves to sionna_mitsuba, the in-repo ingest, which reads sionna_builtin_scene, mitsuba_xml_bundle, and opengert_mitsuba_xml_bundle and converts nothing. Writing that name explicitly is the same answer: every consumer applies the resolution rather than a literal “is it set” test, so spelling the default cannot fork a world cache or move a stored byte. A name no installed distribution registers raises the registry’s own PluginNotFoundError listing what is available, so no refusal is declared for it.

A kind the selected ingest does not declare is refused with geometry_format_not_ingestible before any engine work, naming the ingest, the kind, the URI, and the kinds that ingest reads. Selecting an ingest is therefore a statement about one engine’s capability, not a claim about the asset.

One combination is refused at the configuration boundary. A non-default geometry_ingest beside a radar projection raises geometry_ingest_unroutable_for_projection. The radar renderer builds its world from its own validated params and has no view of the generation configuration, and the scene plan carries no ingest field, so the name cannot reach it; letting the radar path silently use the default while the communications path honoured a configured ingest would build two different worlds under one scene reference, which is the failure plan_world_asset_disagreement exists to prevent. A non-default ingest is a communications-path capability today.

scene.assets, scene.duration_s, scene.multi_rx, and scene.rx_array are deliberately not in the table: this cycle routes no plan fact into any of them, so forbidding them would remove the only working input without providing a replacement. A cross-domain pair therefore points the communications scene.assets.scene_geometry_uri and the plan’s world asset at the same asset by hand, and nothing in this cycle checks that they agree.

Each communications event of the template carries a mandatory comms block: the four identity fields (device_id, transmitter_role, link_id, link_direction), the five emitter-side fields (emitter_key, class_label, bandwidth_hz, frequency_offset_hz, tx_power_dbm), a duration policy restricted to kind: fixed, and an optional sampling block. The block itself is mandatory because ComposerEvent requires the four identity fields, and emitter_key is always authored. The other four are each required to be supplied rather than authored: fixed on the event, or drawn by the matching sampling policy (class_labels, bandwidth_hz, frequency_offset_hz, tx_power_dbm). An event that leaves one neither fixed nor sampled is refused and the error names it. The fixed-duration restriction exists because a plan authors start_s and stop_s while a non-fixed policy draws a duration, and the two agree only by coincidence. The template validator names each failure: a missing block is plan_template_comms_block_missing, another policy kind is plan_duration_policy_not_fixed, and the rule is symmetric, so an active_radar event carrying a comms block is refused under plan_template_comms_block_unexpected rather than having the block ignored.

Eight of the nine are realized-signal stamps: the four identity fields and class_label are carried onto the emitter metadata as authored, emitter_key selects the emitter that generates the IQ, tx_power_dbm becomes that emitter’s transmit power, and frequency_offset_hz is the offset the composer mixes to, so realized_carrier_hz is scene.center_hz plus it. bandwidth_hz is the one exception: it is a placement constraint, the placement-support bound the emitter’s realized bandwidth must not exceed and whose occupied span |frequency_offset_hz| + bandwidth_hz / 2 must fit inside half of scene.bandwidth_hz. It does not resize or filter the emitted signal, so an event declaring 5 MHz whose emitter realizes 200 kHz is accepted and the emitter metadata reports 200 kHz. The authored value is preserved verbatim in the record’s extras.planned_event, so the declared bound and the realized occupancy are both readable and distinguishable.

The model forbids unknown fields and validates assignment. An in-memory caller may omit run and storage; generate_record(...) needs neither. Executors and generate_local_signal_dataset(config) call require_execution_config() and fail with a ConfigError before component resolution if either block is absent. Once a block is provided, run.run_id and storage.path remain required leaf fields. The Dataproc selector additionally requires storage.backend: signal_dataset and a gs:// storage path. Plugin selector names are intentionally not resolved during model validation; resolution happens when the corresponding generic framework component is built.

build_credentials_provider() returns None when no credentials block is configured, otherwise it constructs the selected built-in or entry-point provider. build_augmentation() similarly constructs the optional rfgen.augmentations entry-point class. Both raise ConfigError when the resolved provider or augmentation does not satisfy its declared contract.

RunConfig

RunConfig(*, run_id, num_samples=10_000, shard_size=1_000, seed=42, shard_failure_threshold=1.0) defines the bounded work for one generic generation run.

Field

Type

Required

Validation and behavior

run_id

str

yes

Trimmed, non-empty stable run identifier.

num_samples

int

no

Positive total record count.

shard_size

int

no

Positive records per shard; shard count is ceil(num_samples / shard_size).

seed

int

no

Master seed used to derive deterministic shard/sample seeds.

shard_failure_threshold

float

no

(0, 1] fraction that marks a shard failed.

Boolean values are rejected for numeric fields. A non-divisible sample count is valid and emits a warning because the final shard is partial.

StorageConfig

StorageConfig(*, path, backend=StorageBackend.SIGNAL_DATASET, assets_path=None) configures one record store. The model has exactly three fields. path is required and must be non-empty. A recognized backend string is converted to StorageBackend; a non-empty unrecognized string is retained so an installed dataset-store plugin can resolve it at its applicable boundary.

Field

Type

Required

Default

Behavior

backend

StorageBackend | str

no

signal_dataset

Shipped store or registered store selector.

path

str

yes

Output location, trimmed but otherwise store-governed.

assets_path

str | None

no

None

Optional generic asset-store location.

Signal Dataset accepts local paths and gs:// with the GCS extra.

The model sets extra="forbid", so a configuration carrying compression, chunk_samples, dataset_filename, or record_axis is refused with a Pydantic ValidationError naming the field. Those keys configured a fixed-IQ store layout nothing writes; they were removed rather than accepted and ignored, so a stale config fails at load instead of producing a dataset in an unrequested shape.

Store selectors resolve through BaseDatasetStore.

ExecutorConfig and DataprocServerlessConfig

ExecutorConfig(*, name="local", parallelism=1, dataproc=None) selects the execution backend. name is a trimmed non-empty open selector resolved through rfgen.executors; parallelism is a positive integer used by the Spark backend and ignored by the synchronous local executor. dataproc is required exactly when name="dataproc_serverless" and rejected for every other name.

DataprocServerlessConfig(*, staging_uri, service_account, project="rf-foundation-models", region="us-central1", image_uri=None, labels={}, network=None, subnet=None, spark_properties={}, extra_packages=[]) supplies the shipped remote backend settings. staging_uri must start with gs://; strings must not be blank; network and subnet are mutually exclusive. The remote execution lifecycle is documented in Generation and Dataproc Serverless.

extra_packages names local .whl or .zip files, built on the submitting machine, that carry a use-case package’s entry points onto the Dataproc worker. Each entry is rejected at config-parse time if it is blank, names a remote reference, does not end in .whl or .zip, or collides with another entry’s staged filename; whether the file actually exists, is a valid zip archive, and declares an rfgen.* entry-point group is checked at submission time instead, before the batch is created. See Shipping plugin packages.

Component configuration models

These models are all supported from the concise rfgen.config import path. They remain generic framework configuration, not application policy.

Emitter configuration

EmitterZooConfig, EmitterFamilyConfig, FingerprintConfig, and LoRaConfig describe the emitter pool and optional device-impairment priors. The zoo defaults to no families; composition rejects an empty pool when it needs an emitter. A family requires a closed family, non-empty classes, a positive weight, and an optional non-blank rfgen.emitters selector.

Channel configuration

ChannelConfig and ChannelChainEntry describe the live ordered transform list. Entries use a closed transformation, an optional non-blank selector, and parameters. Validation permits at most one propagation-group entry and requires monotonic transform order.

Scene and receiver configuration

SceneConfig, DensityConfig, EventDurationConfig, ReceiverConfig, MultiRXConfig, ReceiverBackgroundConfig, SceneAssetsConfig, SceneGeometryConfig, GeometryPoseConfig, RTSolverConfig, and StatisticalSolverConfig define the signal grid, event density and duration, receiver topology, optional geometry assets, and propagation solver. Positive physical dimensions and compatible receiver and geometry choices are validated at construction.

Label and annotation configuration

LabelConfig, LabelerSpec, AnnotatorConfig, LLMConfig, and AnnotationRequestConfig select label production and library-level annotation adapters. Plugin selectors must be non-blank; annotation identifiers are non-empty, temperature is in [0, 2], and token counts are positive. The separate unified annotation lifecycle uses AnnotationConfigV1 and run_annotation; see Generate, then annotate.

Augmentation configuration

AugmentationConfig supplies an optional non-blank rfgen.augmentations selector and parameter mapping. It is resolved only by GenerationConfig.build_augmentation().

Storage and execution configuration

StorageConfig defaults to backend: signal_dataset and requires a destination path. Its only other field is assets_path; compression, chunk_samples, dataset_filename, and record_axis are refused by name. The top-level ExecutorConfig selects the local or registered distributed executor; DataprocServerlessConfig supplies the settings for the shipped remote backend.

Scene-renderer configuration

A projection names the renderer that produces what it observes. params.renderer_selector resolves through rfgen.scene_renderers, and params.renderer validates against that class’s ParamsModel. A renderer receives exactly its configured parameters.

Models

Core fields and validation boundary

EmitterZooConfig, EmitterFamilyConfig, FingerprintConfig, LoRaConfig

The zoo defaults to no families; composition rejects an empty pool when it needs an emitter. A family requires a closed family, non-empty classes, positive weight, and an optional non-blank rfgen.emitters selector. Fingerprints hold bounded device-impairment priors; LoRa selects a closed backend enum.

ChannelConfig, ChannelChainEntry

chain is the live ordered transformation list. An entry has a closed transformation, optional non-blank selector, and parameters. Validation permits at most one propagation-group entry and requires monotonic transform order. Legacy name, params, and snr_db_range must retain their documented compatibility defaults.

SceneConfig, DensityConfig, EventDurationConfig, RxArrayConfig, ReceiverConfig, MultiRXConfig, ReceiverBackgroundConfig, SceneGeometryConfig, SceneAssetsConfig, GeometryPoseConfig, RTSolverConfig, StatisticalSolverConfig, ArraySpec, PanelArraySpec

Scene settings define signal-grid values, emitter density, placement parameters, receiver topology, optional geometry assets, and the selected propagation solver. Positive physical dimensions and mutually compatible receiver/geometry choices are validated at construction. See the configuration schema for every field and solver-specific constraint.

PlacementConfig

Closed time/frequency strategy enums plus the non-empty open grid_source selector. The compatibility alias channel_plan_source must agree with grid_source.

LabelConfig, LabelerSpec

Primary and additional non-blank rfgen.labelers selectors with labeler parameters; segmentation FFT and hop controls are positive integers.

AnnotatorConfig, LLMConfig, AnnotationRequestConfig

Library-level annotation requests have non-empty record/template/run identifiers. LLM providers and models are non-blank; temperature is [0, 2], generic token count is positive, and a positive verifier subset requires verifier_llm. scene_report_evidence_v2_max_tokens was removed with the measured-evidence path; a config that still sets it fails validation with a message naming the field. AnnotationRequestConfig.annotation_type likewise names a retired value rather than listing the one permitted member. The command lifecycle is validated separately by AnnotationConfigV1.

AugmentationConfig

Optional non-blank rfgen.augmentations selector and parameter mapping; resolved only by GenerationConfig.build_augmentation().

CredentialsConfig

Non-empty credentials-provider selector and parameter mapping; resolved only by GenerationConfig.build_credentials_provider().

AuditConfig

Bounded generic audit thresholds (snr_mean_error_db, distribution, overlap, occupancy, and cardinality limits); numeric boolean inputs are rejected.

ReferenceHostConfig

Required reference-machine metadata used by documented reproducibility and timing checks.

Validation boundary

Closed-set fields use StrEnum values from rfgen.core.enums; open plugin selectors remain strings and resolve through these exact Python entry-point groups at instantiation:

  • EmitterFamilyConfig.selector and emitter names → rfgen.emitters; ChannelChainEntry.selectorrfgen.channels; ObservationProjectionConfig.params.renderer_selectorrfgen.scene_renderers; LabelConfig.name and LabelerSpec.namerfgen.labelers;

  • AugmentationConfig.selectorrfgen.augmentations; CredentialsConfig.providerrfgen.annotation.credentials; PlacementConfig.grid_sourcerfgen.grid_sources;

  • open time- and frequency-placement selectors → rfgen.time_placement and rfgen.freq_placement; SceneGeometryConfig.geometry_ingestrfgen.geometry_ingests; and

  • ObservationProjectionConfig.selectorrfgen.observation_projections.

GenerationConfig validates cross-field constraints such as a valid signal grid, compatible storage backend/path pairs, and required scene geometry assets. The configuration API validates framework primitives only; external callers own their own dataset expansion, output policy, and validation policy.

For every field, default, and YAML example, see the configuration schema. For a runnable workflow, see Generate a local dataset.