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:
AWGN channel propagation: validated.
Signal Atlas comms-v1 Phase-2 RF physics: validates
RayleighBlockFading,SionnaFlatFading, andSionnaCIRDataset(below), plus three new Sionna-backed emitters, with one fix applied and documented limitations.
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 |
|---|---|---|
abc |
ABC for the |
|
concrete |
Pure-torch additive Gaussian noise; default backend, always available |
|
concrete |
|
|
concrete |
|
|
concrete |
|
|
concrete |
|
|
concrete |
|
|
concrete |
|
|
concrete |
|
|
concrete |
|
|
concrete |
|
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_dbis updated post-call to the requested value.Signals with mean power below
1e-12receive 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.AWGNChannelapplies SNR directly; it does NOT implement the Friis cascaded noise-figure equation or antenna-temperature conversion. For Friis-based receiver noise see theLinearLNANoiseconcrete on the Receiver stages page.
Constructor parameters¶
Name |
Type |
Required |
Default |
Description |
|---|---|---|---|---|
|
float |
no |
|
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 |
|---|---|---|
|
string |
Always |
|
integer |
Always |
|
integer |
Valid paths and padded CIR path slots, respectively. |
|
list, at most 64 rows |
Selected path rows below; |
|
boolean |
|
|
finite linear ratio or |
Selected-path power divided by all valid-path power; |
|
finite linear power or |
|
|
dB or |
|
|
seconds or |
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. |
|
object |
|
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 |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
Selects |
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:
|
Taps |
Tap spacing |
Closed-form RMS delay spread |
|---|---|---|---|
|
6 |
20 ns |
29.8 ns |
|
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 |
|
existing, lists |
the registered entry point is not a |
|
|
the named ingest does not declare |
|
|
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
Transformationenum, and the per-callChannelContext.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.