rfgen.engine

The rfgen.engine package ships the single Group.CHANNEL slot: the BaseChannelPropagation ABC, the default pure-torch AWGNChannel concrete, and nine real Sionna-backed concretes gated behind the rfgen[sionna] extra.

Scientific validation

The AWGN propagation concrete has been scientifically validated against the QPSK BER round-trip from Proakis-Salehi, Digital Communications 5e (eq. 8.2-20). See:

The report covers construct validity, mathematical correctness against cited equations, empirical comparison to published reference numbers, literature grounding, experimental methodology, operating envelope, and documented limitations.

Module surface

rfgen.engine keeps its package initializer empty, so importing the propagation surface never imports the optional Sionna runtime. Import the defining submodule: propagation_generic for the always-available backend, propagation_sionna for the statistical Sionna models, propagation_sionna_rt for the scene solver.

from rfgen.engine.propagation_generic import AWGNChannel
from rfgen.engine.propagation_sionna import RayleighBlockFading, SionnaTDL
from rfgen.engine.propagation_sionna_rt import SionnaRT

# Default: pure-torch AWGN, always available.
channel = AWGNChannel(snr_db=20.0)

# Sionna-backed concretes: zero-arg construction (RayleighBlockFading and
# SionnaCIRDataset), or zero-required-arg construction with optional
# keyword tuning (SionnaFlatFading). All lazy-import the underlying Sionna
# module at construction and raise BackendUnavailableError/ChannelError if
# `sionna` is not installed.
tdl = SionnaTDL()
rayleigh = RayleighBlockFading()

# SionnaRT is the one exception: its constructor does NOT import Sionna
# (it validates the typed ChannelContext geometry inside apply() first),
# so constructing it never requires the sionna extra.
rt = SionnaRT()

The eight non-RT Sionna concretes (SionnaCDL, SionnaTDL, SionnaUMa, SionnaUMi, SionnaRMa, RayleighBlockFading, SionnaFlatFading, SionnaCIRDataset) all live in rfgen.engine.propagation_sionna and raise BackendUnavailableError at instantiation when sionna is not installed (NOT at module import), satisfying the cold-import contract: importing the module never forces Sionna to load. SionnaRT lives in rfgen.engine.propagation_sionna_rt and defers that gate to apply().

Class index

Class

Status

Backend

BaseChannelPropagation

abc

ABC for the Group.CHANNEL slot; pins transformation = Transformation.PROPAGATION

AWGNChannel

concrete

Pure-torch additive Gaussian noise; default backend, always available

SionnaRT

concrete

sionna.rt; differentiable ray tracing

SionnaCDL

concrete

sionna.phy.channel.tr38901.CDL

SionnaTDL

concrete

sionna.phy.channel.tr38901.TDL

SionnaUMa

concrete

sionna.phy.channel.tr38901.UMa

SionnaUMi

concrete

sionna.phy.channel.tr38901.UMi

SionnaRMa

concrete

sionna.phy.channel.tr38901.RMa

RayleighBlockFading

concrete

sionna.phy.channel.RayleighBlockFading; i.i.d. block fading, no delay spread

SionnaFlatFading

concrete

sionna.phy.channel.FlatFadingChannel; correlated flat fading, no delay spread

SionnaCIRDataset

concrete

sionna.phy.channel.CIRDataset; custom power-delay-profile multipath

An internal helper load_tr_38_901_table(table_name: str) -> numpy.ndarray is used by the TR 38.901 byte-equality contract test. It loads one TR 38.901 parameter table from the checked-in CSV blob shipped at rfgen.engine.propagation_sionna.TR_38_901_DATA_DIR and raises FileNotFoundError when a table is missing. It is a test-only helper, not part of the public API, and is not documented as a class below.


class rfgen.engine.propagation_generic.BaseChannelPropagation

class BaseChannelPropagation(BaseChannel):
    transformation: ClassVar[Transformation] = Transformation.PROPAGATION

    @abstractmethod
    def apply(self, signal: Signal, ctx: ChannelContext) -> Signal: ...

ABC for the single Group.CHANNEL slot. Concrete subclasses model path loss, multipath, shadowing, Doppler, or pure additive noise. The ABC pins transformation = Transformation.PROPAGATION (integer value 21); subclasses inherit the pin.


class rfgen.engine.propagation_generic.AWGNChannel

class AWGNChannel(BaseChannelPropagation):
    def __init__(self, *, snr_db: float = 20.0) -> None: ...

Pure-torch additive white Gaussian noise channel; the default backend that always ships. Adds complex Gaussian noise such that the realised SNR equals the snr_db argument against the input signal’s measured power.

Math

The per-rail variance is sigma**2 = signal_power / (2 * SNR_linear) (Sklar, Digital Communications, 2nd ed., Prentice-Hall, 2001, Ch. 3).

Behaviour

  • The metadata snr_db is updated post-call to the requested value.

  • Signals with mean power below 1e-12 receive noise computed from the floor rather than the actual power; the SNR contract does not hold in that sub-floor regime. The floor exists to prevent division by zero on all-zero inputs.

  • AWGNChannel applies SNR directly; it does NOT implement the Friis cascaded noise-figure equation or antenna-temperature conversion. For Friis-based receiver noise see the LinearLNANoise concrete on the Receiver stages page.

Constructor parameters

Name

Type

Required

Default

Description

snr_db

float

no

20.0

Target post-noise SNR in dB. The realised noise variance is computed from the input signal’s mean power against this target.

Method: apply

apply(signal, ctx) reads ctx.rng for the noise draws (single fused torch.randn(2, n, ...) call) and returns a new Signal with noise-augmented IQ and metadata.snr_db = self.snr_db. Appends a TransformationLogEntry with name="AWGNChannel", group=Group.CHANNEL.value, transformation=Transformation.PROPAGATION.value, params={"snr_db": self.snr_db}.


Sionna-backed propagation backends

The six 3GPP/RT Sionna concretes share a common shape: each has a zero-argument constructor. schema() reports EmptyParams (no discoverable config leaves), so the constructors accept no **kwargs; a typo’d config key fails loudly at Hydra/YAML validation rather than being silently swallowed. Solver knobs are carried through ChannelContext.rt_solver_params, not through constructor arguments: SionnaRT reads a RTSolverConfig, the other five read a StatisticalSolverConfig.

All six run a real Sionna solve on apply() – none are stubs. SionnaCDL, SionnaTDL, SionnaUMa, SionnaUMi, SionnaRMa lazy-import sionna at construction via the internal _SionnaPropagationBase.__init__. SionnaRT is the one exception: its constructor does NOT import Sionna (it validates the typed ChannelContext geometry inside apply() first).

The three statistical-fading concretes below (RayleighBlockFading, SionnaFlatFading, SionnaCIRDataset) share _SionnaPropagationBase with the six above (the same RNG scoping, StatisticalSolverConfig CIR-window fields, and provenance tail), but do not share the six’s zero-discoverable-config-leaf shape: SionnaFlatFading and SionnaCIRDataset take real, non-empty constructor keyword arguments (see each class below), and none of the three reads ChannelContext.rt_solver_params for its fading-specific parameters. All three live in rfgen.engine.propagation_sionna alongside the six above and are imported from that module directly, like every other statistical Sionna backend on this page.

The Sionna shim layer is _SionnaPropagationBase, an internal abstract subclass marked private by its leading underscore. SionnaUMa/SionnaUMi/SionnaRMa additionally share _SionnaSystemLevelBase (topology-based, no scene); SionnaTDL/SionnaCDL share _SionnaLinkLevelBase (no topology at all). Every one of the six reuses the SAME discrete-time channel conversion helper (_synthesize_time_domain_iq, internal), so the CIR-to-IQ DSP is written and tested once, not once per backend.

class rfgen.engine.propagation_sionna_rt.SionnaRT

class SionnaRT(_SionnaPropagationBase):
    _sionna_path = "sionna.rt"
    requires_geometry: ClassVar[bool] = True
    solver_backend: ClassVar[str] = "sionna-rt"

    def __init__(self) -> None: ...

Ray-traced propagation backed by sionna.rt, forwarding only los, specular_reflection, diffuse_reflection, and refraction. Sionna’s diffuse scattering is optional and is enabled only through diffuse_reflection; RFGen does not expose or pass Sionna diffraction, edge-diffraction, or diffraction-lit-region controls. The resulting solver outputs are not field-calibrated. Requires the rfgen[sionna] extra to run apply(). Unlike the five 3GPP concretes, SionnaRT.__init__ does NOT import Sionna: it validates the typed ChannelContext geometry inside apply() before importing the backend, so malformed scene wiring fails without the optional extra installed. The apply() solve returns the authoritative GeometryProvenance (real tx_array_id / rx_array_id, asset hashes, backend versions) on the propagated signal’s metadata.

Solved-path evidence

metadata.extras["rt_channel"]["path_evidence"] is a bounded rfgen-rt-path-evidence-v1 summary of the solved Sionna channel impulse response (CIR): the complex path coefficients and their delays. RFGen reads the path-validity mask from Sionna’s Paths.valid contract and the coefficients/delays from its Paths.cir output; it does not infer paths or reimplement Sionna’s PathSolver. No Sionna API emits RFGen’s bounded rfgen-rt-path-evidence-v1 schema: the aggregation, ordering, and 64-row truncation below are RFGen’s custom metadata contract over those solver-native outputs. The current single-link, single-element-array contract accepts only the singleton leading axes of Sionna’s synthetic [rx, tx, path] or non-synthetic [rx, rx_ant, tx, tx_ant, path] validity layouts and aligns them to the CIR’s trailing path axis. Other link/stream layouts fail rather than being reduced implicitly.

The top-level fields are:

Field

Type and units

Meaning

schema_version

string

Always rfgen-rt-path-evidence-v1.

cir_snapshot_time_step_index

integer

Always 0: later Doppler-evolved time steps are not summarized.

resolved_path_count, path_slot_count

integer

Valid paths and padded CIR path slots, respectively.

path_components

list, at most 64 rows

Selected path rows below; path_components_truncated states whether valid rows were omitted.

path_components_truncated

boolean

true if and only if resolved_path_count exceeds the fixed 64-component cap; otherwise false.

retained_path_power_fraction

finite linear ratio or null

Selected-path power divided by all valid-path power; null when that total is zero or there are no valid paths.

incoherent_path_gain_linear

finite linear power or null

sum_i |a_i|^2 over valid paths.

incoherent_path_gain_db, path_loss_db

dB or null

10 log10 of that gain and its negative, respectively. path_loss_db is a solver-coefficient-derived proxy: it includes Sionna antenna response, excludes transmit power, and is not a calibrated or measurement-comparable path-loss estimate.

first_arrival_delay_s, mean_excess_delay_s, rms_delay_spread_s, max_excess_delay_s

seconds or null

First valid arrival and power-weighted excess-delay summaries. These are RFGen engineering summaries of solver coefficients, not a 3GPP-defined estimator or measurement-calibrated channel statistic.

normalization

object

cir_coefficients_normalized=false, delays_normalized, tx_power_embedded=false, and path_loss_includes_antenna_response=true.

For each valid path, RFGen forms p_i=|a_i|^2, takes the first arrival tau_0=min_i tau_i, and uses d_i=tau_i-tau_0. When total power is positive, mean_excess_delay_s=sum_i(p_i d_i)/sum_i p_i and rms_delay_spread_s=sqrt(sum_i(p_i(d_i-mean)^2)/sum_i p_i); the maximum is max_i d_i. If no path is valid, every gain/delay statistic is null, the component list is empty, and the count is zero. If valid paths have zero total power, gain and mean/RMS delay are null, while the finite maximum excess delay is still reported.

Deterministic verification route

The custom RFGen reduction is verified with deterministic, synthetic CIR arrays rather than by presenting it as an additional physical model: tests/unit/test_propagation.py::test_resolved_path_evidence_uses_only_library_valid_paths checks that library validity controls the aggregate evidence; tests/unit/test_propagation.py::test_resolved_path_evidence_default_cap_preserves_full_summaries checks the 64-row truncation and retained/full aggregates; tests/unit/test_propagation.py::test_resolved_path_evidence_zero_path_compatibility checks the zero-path representation; and tests/unit/test_propagation.py::test_resolved_path_evidence_rejects_finite_coefficients_with_overflowing_power checks that unrepresentable custom arithmetic fails closed. These tests verify the documented metadata transformation and JSON boundary, not a new claim about Sionna’s physical solver or measurement realism.

Each component has path_index (integer), coefficient_real and coefficient_imag (finite dimensionless coefficient parts), power_linear, and reported_delay_s, absolute_delay_s, and excess_delay_s (seconds). Components are ordered by descending power_linear, breaking a tie by the original ascending path_index, and only the first 64 are retained. The fixed 64-component limit is an RFGen engineering choice with no external threshold basis. All valid paths, not just retained rows, contribute to the aggregate summaries; therefore per-path evidence is incomplete when path_components_truncated is true. With delay normalization, Sionna reports delays relative to the first arrival: absolute_delay_s and first_arrival_delay_s are null, while reported_delay_s and excess_delay_s remain relative. Non-finite values or reductions that cannot be represented as finite JSON cause apply() to raise a structured ChannelError rather than publish NaN or infinity. For the pipeline context in which a CIR is applied to I/Q, see Channels and Concepts / Channels.

class rfgen.engine.propagation_sionna.SionnaUMa

class SionnaUMa(_SionnaSystemLevelBase):
    _sionna_path = "sionna.phy.channel.tr38901.UMa"
    _model_name = "UMa"
    _requires_o2i = True
    solver_backend: ClassVar[str] = "sionna-uma"

    def __init__(self) -> None: ...

3GPP TR 38.901 Urban Macro scenario. Real statistical propagation: builds a one-UT/one-BS network topology from ctx.tx_pose/ctx.rx_params.rx_pose and Sionna’s PanelArray (from StatisticalSolverConfig.ut_array/bs_array), then calls sionna.phy.channel.tr38901.UMa for a real channel realization.

from rfgen.config.scene import PanelArraySpec, StatisticalSolverConfig
from rfgen.engine.propagation_sionna import SionnaUMa

solver = StatisticalSolverConfig(
    direction="downlink",
    o2i_model="low",
    bs_array=PanelArraySpec(num_rows_per_panel=4, num_cols_per_panel=4),
)
# ctx.tx_pose / ctx.rx_params.rx_pose come from the scene composer's placed
# TX/RX poses; solver is threaded through ctx.rt_solver_params.
out = SionnaUMa().apply(signal, ctx)
out.metadata.extras["statistical_channel"]
# {"num_paths": 24, "carrier_frequency_hz": 3.5e9, "direction": "downlink",
#  "model": None, "o2i_model": "low", "cir_a_shape": (1, 1, 1, 1, 24, 1),
#  "cir_tau_shape": (1, 1, 24), "tap_l_min": -6, "tap_l_max": 9,
#  "dominant_path_delay_s": 2.37e-07, "dominant_path_gain_linear": 3.82e-06}

direction ("downlink" default or "uplink") decides which pose plays UT vs. BS. o2i_model (outdoor-to-indoor loss) is forwarded unchanged; RMa (below) has no such knob. aoa_deg/aod_deg are deliberately absent from extras["statistical_channel"]: Sionna’s per-path Rays cluster/sub-ray axes do not decompose 1:1 onto cir_a’s num_paths axis, so there is no clean dominant-path angle the way SionnaRT’s rt_channel.aoa_deg provides.

class rfgen.engine.propagation_sionna.SionnaUMi

class SionnaUMi(_SionnaSystemLevelBase):
    _sionna_path = "sionna.phy.channel.tr38901.UMi"
    _model_name = "UMi"
    _requires_o2i = True
    solver_backend: ClassVar[str] = "sionna-umi"

    def __init__(self) -> None: ...

3GPP TR 38.901 Urban Micro / street-canyon scenario. Same topology/config contract as SionnaUMa above.

class rfgen.engine.propagation_sionna.SionnaRMa

class SionnaRMa(_SionnaSystemLevelBase):
    _sionna_path = "sionna.phy.channel.tr38901.RMa"
    _model_name = "RMa"
    _requires_o2i = False
    solver_backend: ClassVar[str] = "sionna-rma"

    def __init__(self) -> None: ...

3GPP TR 38.901 Rural Macro scenario. Same topology/config contract as SionnaUMa, except RMa has no outdoor-to-indoor loss model: StatisticalSolverConfig.o2i_model is ignored.

class rfgen.engine.propagation_sionna.SionnaTDL

class SionnaTDL(_SionnaLinkLevelBase):
    _sionna_path = "sionna.phy.channel.tr38901.TDL"
    _model_name = "TDL"
    solver_backend: ClassVar[str] = "sionna-tdl"

    def __init__(self) -> None: ...

3GPP TR 38.901 TDL (tapped-delay-line) model. Real statistical propagation with no network topology at all – only a scenario letter, a delay spread, and a carrier frequency.

from rfgen.config.scene import StatisticalSolverConfig
from rfgen.engine.propagation_sionna import SionnaTDL

solver = StatisticalSolverConfig(model="A", delay_spread_s=100e-9)
out = SionnaTDL().apply(signal, ctx)  # ctx.rt_solver_params = solver
out.metadata.extras["statistical_channel"]["model"]  # "A"

TDL has no antenna-array or direction concept (single-antenna, direction-agnostic by construction); StatisticalSolverConfig.ut_array/bs_array/direction are ignored.

class rfgen.engine.propagation_sionna.SionnaCDL

class SionnaCDL(_SionnaLinkLevelBase):
    _sionna_path = "sionna.phy.channel.tr38901.CDL"
    _model_name = "CDL"
    solver_backend: ClassVar[str] = "sionna-cdl"

    def __init__(self) -> None: ...

3GPP TR 38.901 CDL (clustered-delay-line) model. Same no-topology contract as SionnaTDL, but accepts direction and reuses StatisticalSolverConfig.ut_array/bs_array for its antenna arrays – Sionna’s own ut_array=None/bs_array=None CDL defaults do not work against the installed Sionna version (tx_array.num_ant raises on None internally), so real arrays are always supplied.


Statistical fading backends (non-3GPP)

These three concretes model generic statistical fading, not a named 3GPP scenario. Use them when a dataset needs a controllable single-tap or custom-multipath fading axis without committing to a specific 3GPP deployment scenario. See the Phase-2 physics validation for the statistical evidence (Rayleigh-distributed amplitude, correlation-coefficient tracking, and profile-consistent delay spread).

class rfgen.engine.propagation_sionna.RayleighBlockFading

class RayleighBlockFading(_SionnaPropagationBase):
    solver_backend: ClassVar[str] = "sionna-rayleigh-block"

    def __init__(self) -> None: ...

Wraps sionna.phy.channel.RayleighBlockFading directly: draws one normally-distributed complex gain at zero delay per call and tiles it over the requested time steps (one coefficient per coherence block, no delay spread). Zero-argument constructor; no configurable parameter. Its per-draw gain magnitude is genuinely Rayleigh-distributed (confirmed by a Kolmogorov-Smirnov goodness-of-fit test in the linked validation), not merely “non-constant.” Deliberately static-only: no resampler is supplied for the CIR-to-IQ conversion, so a caller requesting StatisticalSolverConfig.cir_num_time_steps > 1 gets a clear ChannelError naming this backend rather than a silently approximated time-varying block.

out_signal = RayleighBlockFading().apply(signal, ctx)

class rfgen.engine.propagation_sionna.SionnaFlatFading

class SionnaFlatFading(_SionnaPropagationBase):
    solver_backend: ClassVar[str] = "sionna-flat-fading"

    def __init__(
        self,
        *,
        num_fading_blocks: int = 16,
        correlation: float = 0.7,
        correlation_model: Literal["kronecker", "per_column"] = "kronecker",
    ) -> None: ...

Wraps sionna.phy.channel.FlatFadingChannel to exercise Sionna’s spatial correlation machinery (KroneckerModel/PerColumnModel, built from exp_corr_mat) against a single-antenna Signal by repurposing the “receive antenna” axis as a TIME-BLOCK axis: one draw yields num_fading_blocks gains, correlated across the block axis by the requested model, instead of num_fading_blocks independent draws. RayleighBlockFading above is the i.i.d.-across-blocks case; correlation is this backend’s whole reason to exist, not an optional extra. Deliberately bypasses the CIR-to-IQ FIR/sinc conversion the 3GPP concretes use: each contiguous block of ceil(n / num_fading_blocks) IQ samples is multiplied directly by its own drawn gain, since a flat-fading channel has no multipath to convolve.

Parameter

Type

Default

Constraint

num_fading_blocks

int

16

>= 1; raises ChannelError otherwise

correlation

float

0.7

-1.0 < correlation < 1.0 (exp_corr_mat’s positive-semi-definiteness precondition); raises ChannelError otherwise

correlation_model

Literal["kronecker", "per_column"]

"kronecker"

Selects sionna.phy.channel.KroneckerModel or PerColumnModel

apply() records num_fading_blocks, block_len_samples, correlation, correlation_model, dominant_gain_linear, and carrier_frequency_hz under metadata.extras["flat_fading_channel"]. Empirically, the inter-block correlation coefficient tracks the requested correlation within 0.08 absolute tolerance (see the linked validation).

class rfgen.engine.propagation_sionna.SionnaCIRDataset

class SionnaCIRDataset(_SionnaPropagationBase):
    solver_backend: ClassVar[str] = "sionna-cir-dataset"

    def __init__(self, *, pdp_profile: str = "custom_pdp_short_office") -> None: ...

Wraps sionna.phy.channel.CIRDataset over one declared, generic (non-3GPP) exponential power-delay profile (Rappaport, Wireless Communications: Principles and Practice, 2nd ed., Sec. 5.4). The two declared profiles are analytically generated from the classic exponential PDP model, not transcribed standards-body measurement values:

pdp_profile

Taps

Tap spacing

Closed-form RMS delay spread

"custom_pdp_short_office" (default)

6

20 ns

29.8 ns

"custom_pdp_long_urban_macro"

8

200 ns

365.1 ns

An unknown pdp_profile value raises ChannelError. The realized CIR’s tap count matches the declared profile exactly (confirmed in the linked validation); the two profiles’ RMS delay spreads differ by more than 10x, well past the framework’s >=10% distinctness bar for declared custom profiles. Deliberately static-only for the same reason as RayleighBlockFading: CIRDataset documents its own num_time_steps as “ignored, uses the configured num_time_steps” (fixed here at construction to 1), so a caller requesting cir_num_time_steps > 1 gets the same clear ChannelError rather than a silent no-op.

out_signal = SionnaCIRDataset(pdp_profile="custom_pdp_long_urban_macro").apply(signal, ctx)

TR 38.901 data helper

rfgen.engine.propagation_sionna.TR_38_901_DATA_DIR: pathlib.Path
rfgen.engine.propagation_sionna.load_tr_38_901_table(table_name: str) -> numpy.ndarray

TR_38_901_DATA_DIR is a public module attribute pointing at the checked-in CSV blob (under src/rfgen/engine/data/tr_38_901/). load_tr_38_901_table(name) is an internal, test-only helper that loads one parameter table from that directory using numpy.loadtxt(..., delimiter=",", dtype=numpy.float64). Raises FileNotFoundError when a table is missing so the contract test can pytest.skip cleanly.


The geometry ingestion boundary: rfgen.engine.ingest

For why this boundary is a refusal rather than a converter, see Concepts / External Scene Seams.

class GeometryIngest(ABC):
    ingests: ClassVar[frozenset[GeometryAssetKind]]
    name: ClassVar[str]

    @classmethod
    def converter_version(cls) -> str | None: ...
    def load(self, rt: Any, ref: GeometryAssetRef) -> Any: ...

One engine’s declaration of what world geometry it can read. ingests is declared, not inferred, and the caller checks membership before any engine work happens, so a kind no ingest reads costs nothing and fails by name. The check is caller-side for the reason require_shared_world_qualified records one seam over: the ClassVar is on the ABC, so one assertion covers every registered ingest, including out-of-tree ones this repository never sees.

The one shipped implementation, SionnaMitsubaIngest, is registered under the rfgen.geometry_ingests entry-point group as sionna_mitsuba and declares SIONNA_BUILTIN_SCENE, MITSUBA_XML_BUNDLE, and OPENGERT_MITSUBA_XML_BUNDLE. Its converter_version() returns None, which is the honest answer rather than a placeholder: Mitsuba reads those three natively and nothing is converted. DEEPMIMO_EXPORT is deliberately outside the set.

Selection is by name, and never by scanning. scene.geometry.geometry_ingest names one ingest; that one name is resolved through the registry, which imports that one module, and the capability is then asserted. A dispatcher that instead picked “the ingest declaring this kind” would have to read every registered ingest’s ClassVar, which means importing arbitrary third-party code as a side effect of loading geometry. An unregistered name raises the registry’s own PluginNotFoundError listing available().

load_scene_for_ref(rt, ref, *, geometry_ingest=None) is the dispatcher. It refuses in exactly three ways, plus whatever the selected ingest’s own load raises:

Situation

Error

Code

the configured name is not registered

PluginNotFoundError

existing, lists available()

the registered entry point is not a GeometryIngest subclass

ValidationError

geometry_ingest_not_a_geometry_ingest

the named ingest does not declare ref.kind

ValidationError

geometry_format_not_ingestible

The rows are in the order they fire. The middle one is the refusal an out-of-tree plugin author meets while wiring an entry point: the name resolved, the module imported, and the object it pointed at was something other than a GeometryIngest. resolve_geometry_ingest(name) performs that check and returns the class; resolve_geometry_ingest_name(configured) returns the name that will be used, which is what the world_ingest disclosure below is defined against; and DEFAULT_GEOMETRY_INGEST is the name used when the configuration declares none, "sionna_mitsuba".

Registering an out-of-tree ingest. Subclass GeometryIngest, declare name, ingests, and converter_version(), register the class under the rfgen.geometry_ingests entry-point group, and name it in scene.geometry.geometry_ingest. When the resolved name is not sionna_mitsuba, two keys appear in SignalMetadata.extras: world_asset_kind, the kind that was ingested, and world_ingest, the string f"{name}@{converter_version() or 'none'}". Their absence is the disclosure for the default: an absent world_ingest means the in-repo Mitsuba ingest, converting nothing. They live in the extras mapping rather than on GeometryProvenance because that class is a frozen dataclass serialized through a recursive asdict, so every field it declares reaches every record that carries it, and absence is only expressible in a mapping.

No conversion is shipped, and that is a judgment call rather than an omission. The installed Mitsuba cannot read USD. A converter would have to tessellate geometry, which OpenUSD largely provides, and map materials, which it does not: ITURadioMaterial is parameterized by an ITU material type and a thickness, while a USD stage carries UsdPreviewSurface visual parameters from which permittivity and conductivity cannot be derived. A converter would have to invent the radio material for every prim, and an invented material silently changes every reflection coefficient, path loss, and delay spread in a corpus. That is a validation problem with its own literature, not a seam. What this module ships is the place where it will land, addressable and versioned.


See Also

  • Channels: common ABC, the Transformation enum, and the per-call ChannelContext.

  • Receiver stages: the receiver-side concretes (including LinearLNANoise, which implements thermal noise via the kTBF formula for an integrated receiver-noise model).

  • Concepts / Channels: mental model and data flow for the 4-group pipeline.

  • Concepts / External Scene Seams: why the geometry ingestion boundary refuses USD rather than converting it.