Note
API signatures. This page includes contract excerpts rather than complete copy-paste programs; use the documented Golden Paths for executable workflows.
rfgen.nodes¶
A node is one typed operation in an RF scene: it can create a waveform, apply a channel or receiver effect, combine signals, resolve authored plan facts, or project labels. This boundary keeps scientific operators independent from the graph compiler that schedules them.
StagedGraphRuntime executes an installed pipeline’s compiled staged graph without owning any
use-case topology or publication schema.
Common contract¶
Node(ABC) declares role: RoleKind and implements
spec(self) -> NodeSpec. A NodeSpec contains validated Pydantic parameters,
typed input/output Port values, wiring Ref values, and a ReproClass.
When.PLAN values exist after materialization; When.REALIZED values exist
after execution. The compiler owns staging and scheduling and is not an
overridable node surface.
A NodeSpec may also carry receiver_evidence, a tuple of
ReceiverEvidenceRequirement values. Each requirement says that one input’s
evidence describes a particular receiver. The graph binder resolves that claim
statically over the authored producer chain and every container alias, so a
mismatch fails rfgen validate rather than surviving to generation. Select and
conditional branches must resolve to one scope-qualified receiver authority;
subgraph inputs resolve at the call site; repeat-local receiver authorities
remain distinct. Ordinary complex-voltage transforms preserve receiver
evidence on signal outputs, and combiners union every signal-input authority.
Receiver authority originates only at a PLAN producer whose outputs include
both receiver-index-bearing facts and a ReceiverEntityRef; a parameter merely
named receiver_index on a transform is not evidence and cannot replace its
input signal’s authority.
An alias whose canonical type carries receiver_index but cannot be traced to
an authored receiver fails closed. Only a truly partial type without that field
retains the documented no-claim limitation.
ReceiverBoundaryLineageRequirement is the public label-side form of the
stronger causal check. Its measurement_input names boundary facts,
signal_inputs names the numerator contributions, and noise_input names the
denominator. Validation requires the measurement and every numerator to resolve
to the same singleton checked ReceiverInputBoundaryProducer; the noise
evidence must resolve through its declared boundary citation to that same
singleton. The resolver follows select, conditional, repeat, and subgraph
aliases. Ordinary complex-voltage transforms preserve boundary ancestry for
signal outputs only; they cannot turn an arbitrary facts output into boundary
evidence. Equal receiver ids, equal payloads, or an old boolean on an arbitrary
class do not satisfy this producer identity check.
receiver_input_boundary is the public evidence seam used by both noise
families and emitter_snr. The boundary binds the final propagation signal and
facts from one immediate producer, the authoritative link endpoints, and one
receiver_measurement PLAN value. Its signal and facts name the same receiver,
the receiver_input measurement plane, and the exact application grid. Target-
SNR AWGN and thermal noise require their signal and boundary_facts from that
same boundary producer. emitter_snr requires both numerator signal evidence
and denominator noise evidence to trace to the same boundary receiver and
plane. Rewiring only one of those edges is therefore a type/binding error, even
when every scalar value would otherwise be finite.
Because PropagationEndpointFacts is the exact canonical endpoint contract, a
producer must bind each role’s identity and pose to the same scope-qualified
setup PLAN owner. Its nested transmitter and receiver identity projections must
exactly match the corresponding bound TransmitterEntityRef and
ReceiverEntityRef types, including their singleton identity vocabularies. A
no-input plugin cannot publish a type-correct endpoint payload and thereby
self-author custody, nor can a plugin consume receiver B while declaring
endpoint A. Empty, unresolved, mixed, or crossed identity/pose authority fails
closed through container aliases. The partial third-party no-claim limitation
applies only to genuinely partial noncanonical types, never to exact canonical
endpoint facts.
Materialization completes the same custody contract at the value level. The common PLAN-output validator canonical-codec compares each emitted transmitter and receiver identity and pose with the resolved bound inputs, validates the emitted link reference, and requires its link id to equal the node’s declared parameter. Declaring the truthful types while returning a second pose or entity payload is therefore refused before plan identity is computed.
PlanNode.resolve() must not mutate its bound inputs. MATERIALIZE supplies
canonical codec clones, retains the pre-call canonical bytes, and rejects any
post-call difference. It also canonical-clones accepted outputs before storing
them in the resolved plan. Nested mappings, tensors, arrays, ragged payloads,
and ordinary scalar/enum fields therefore share one isolation rule: neither an
in-place consumer nor a plugin-retained return value can mutate another node’s
stored PLAN output.
Realized roles implement
evaluate(self, inputs: dict[str, Value], seed: torch.Generator) -> dict[str, Value].
Source.from_params(params) validates params_type and has no input ports.
Transform.from_params(params, refs=..., input_types=..., input_whens=...)
requires every dependency the class names in dependency_names, which is empty
on the role base – core does not presume a transform consumes a signal.
WaveformTransform is the subclass that declares dependency_names = ("signal",), and its from_params(params, signal_type=..., refs=..., input_types=..., input_whens=...) resolves that TensorType from the bound
input when the keyword is omitted. input_whens carries the plan/realized tag
the graph bound each port at; see “Transform staging” below, because a
transform that consumes a plan fact must declare it or it will not construct.
Labels may derive fields only from bound evidence. Plan nodes resolve plan facts
during materialization. Every output key and ValueType must exactly match
spec().
Every role registers through an entry-point group: rfgen.nodes.source,
rfgen.nodes.transform, rfgen.nodes.plan, rfgen.nodes.allocator,
rfgen.nodes.label, and rfgen.nodes.combiner. Core’s own selectors are
registered inside those same groups and are not overridable; a third-party
implementation is added under a name of its own. An implementation must define
a concrete Pydantic params_type, constructor, spec, role method, and
focused deterministic/type-compatibility tests. A WaveformSource additionally
declares bandwidth_semantics_kind and names the parameter carrying its
resolved occupied bandwidth in occupied_bandwidth_param (or overrides
occupied_bandwidth_hz() when the width is computed rather than authored).
Graph authors obtain the placement budget through
WaveformSource.resolve_occupied_bandwidth_hz(params). Its default constructs
the source and reads occupied_bandwidth_hz(). A backend-bound third-party
source whose constructor resolves coding, downloads resources, or imports an
optional runtime must override the class method with a backend-independent
calculation under the source’s RF concept module. The override must validate
the relevant parameters and return exactly the width the realized node later
publishes; it may not use an approximate use-case formula. Contract tests must
compare resolver, constructed-node width, emitted bandwidth facts, and scene
placement budget wherever the backend is available, plus exercise the resolver
with that backend absent.
WaveformCandidate copies one source selector and its parameters into canonical
immutable JSON. A non-empty WaveformCandidateTable assigns dense ordered
indices, allowing a plan to select a finite choice without naming an import
path. WaveformCandidateProvider is the staging seam;
RegisteredWaveformCandidateProvider is its stock implementation over the
existing rfgen.nodes.source registry. It validates every selector, source
role, and parameter schema before returning StagedWaveformCandidates, without
constructing sources that still require PLAN inputs. Creation accepts the
canonical typed NodeBinding, requires its selector and parameters to match the
staged candidate, and preserves its PLAN wires through Source.from_binding.
Parameter-only sources may omit the binding. Workers select only by an index in
the staged table. Candidate
tables do not define Signal Atlas labels, waveform physics, sample-grid
conversion, randomness, resource limits, provenance, or record fields.
Whole-source staged execution uses BoundedCandidateSourceParams and
BoundedCandidateSourceOperator, followed immediately by
BoundedSourceResamplerOperator. WaveformSelectionPlan contains immutable
WaveformSelectionGroup values: core samples an equal-probability parent and
then an equal-probability child, while the use case owns the groups.
BoundedWaveformRuntimeService returns a transient CandidateWaveformFact and
refuses an unstaged source, changed installed implementation, or wrong H2
consumer. select_waveform_candidate uses independent structural Philox streams
for parent and variant selection.
The serialized worker boundary is WaveformExecutionPlan, containing one exact
WaveformExecutionSlot per candidate-source/resampler pair.
bounded_waveform_provider constructs the production provider and
waveform_resource_accounting discovers the open accounting role. Core sources
use CoreBasebandResourceAccounting; core_waveform_accounting resolves the
implementation-owned allocation model and core_waveform_resource_authority
derives the finite native grid, H2 output grid, source support, workspace ceiling,
and provider dimensions. No use case supplies memory constants. Candidate
execution audits the observable output and analytic dimensions against that
authority after construction.
rfgen.nodes.values owns tensor axes, units, semantic qualifiers, compatibility,
stable type bytes, codecs, and runtime Value objects. Detection schemas and
native projections live in rfgen.nodes.label; geometry types live in
rfgen.nodes.plan.geometry.
rfgen.nodes.label.projection is the in-memory reader for those detection
fields, mirroring rfgen.storage.sds.accessors on the persisted side. A record
may carry more than one family of boxes, so it offers three questions rather
than one: box_families returns the family prefixes a record actually carries,
detection_boxes reads one named family (defaulting to ground_truth.boxes),
and all_detection_boxes reads every family: which is what “the boxes in this
record” means, and what yolo_boxes and segmentation_mask consumers want.
Both readers accept a family cut into any number of rows, since a family
emitted by a bounded repeat carries one row per entity. See
the label schema.
Entity-group frame owner¶
entity_group_frame (EntityGroupFramePlan, configured by
EntityGroupFrameParams) is the one PLAN-staged owner of a framed scene’s
ordered entity-group vocabulary. It publishes one exact EntityGroupFrame and
one EmitterGroupPopulationAllocation for each named population. The frame
embeds the closed v1 version, the exact indexed role emitter, and the
complete ordered catalog. Each allocation embeds that frame plus its singleton
group code/name, population name, dense index_base, and positive extent.
Ranges are assigned in authored population order within each group, so a fixed
population and a repeated population can share a group without both minting
local index zero. Catalog order and allocation ranges participate in structural
type identity rather than travelling as unrelated integer conventions.
Each population declaration is an EntityGroupPopulationParams value naming
its group, its scene-unique population, and its positive maximum extent.
entity_group_frame_type, entity_group_frame_payload,
entity_group_population_type, and entity_group_population_payload are the
public construction seam. require_entity_group_frame_type and
require_entity_group_population_type reject noncanonical versions, roles,
codes, ranges, and allocations whose code/name does not belong to the embedded
frame; entity_group_names and entity_group_population read the authoritative
vocabulary and range. Group names must round-trip exactly through the SDS
<group>.<ordinal>.<member> grammar; dotted nonnumeric namespaces such as
cuas.drones are valid, while whitespace, path separators, and numeric terminal
segments are not.
emitter_group_identity_fields and emitter_group_identity_payload derive the
checked pair and retain the exact allocation for nested producer facts.
emitter_group_row_identity_fields, emitter_group_row_identity_payload, and
require_emitter_group_row_identity are the corresponding flat-struct seam:
they carry the shared frame catalog into homogeneous ragged rows without
nesting an allocation or duplicating a contribution waveform.
The compatibility representation is absence of a frame. There is no empty
EntityGroupFrame structural variant: an unframed graph keeps the exact label
parameters, field set, canonical type bytes, and plan identity it had before
framing. EntityGroupFrameProducer is the public owner marker, but the marker
alone grants no authority: graph validation requires one input-free PLAN node,
one exact frame, dense complete population outputs, and nonoverlapping ranges.
There may be at most one such nonempty authority in a scene. A framed consumer
declares EntityGroupAllocationRequirement on its exact PLAN input; lexical
aliases must resolve to that same scope-qualified owner through every
select/conditional branch, repeat witness, and subgraph call. An unmarked node
cannot preserve authority by copying an allocation’s shape, and a multi-input
requirement must resolve each input to a distinct population output rather than
reusing one allocation under two emitter-population claims. Every traversed
container or lexical wire remains PLAN-staged and declares the exact canonical
type of that authoritative population output.
entity_group_membership (EntityGroupMembershipPlan) owns dataset-root
metadata for indexed record families whose entity group is invariant across a
dataset. EntityGroupMembershipParams supplies the canonical group catalog and
an ordered family list. Each EntityGroupFamilyMembershipParams chooses one
representation: one group for a whole variable-cardinality family, or dense
ordered half-open EntityGroupItemRangeParams intervals for a fixed mixed
family. entity_group_membership_type and
entity_group_membership_payload construct the immutable v1 authority;
require_entity_group_membership_type rejects crossed frames, unknown groups,
and noncanonical structures. The authority is not repeated in each record.
A framed transmitter_setup binds one allocation and a strict integer
population_local_index. It derives the global index as
index_base + population_local_index, checks the population extent and int64
range, and publishes the pair (emitter_group_id, transmitter_index) together
with the exact allocation on its TransmitterEntityRef. The ordinary unframed
form has no allocation input or local-index parameter and retains its prior
schema and parameter dump exactly. Framed source_provenance consumes that
exact checked transmitter identity; it cannot independently author a group or
index. Pose-free and posed propagation facts preserve the same allocation in
their transmitter/link projection. Atomic receiver-incident rows flatten only
the shared frame vocabulary, so rows from different populations remain
homogeneously collectable, and order by the composite group/index identity.
Distinct causal contributions keep their contribution and directed-link identities
through aggregation, including when more than one path belongs to the same framed
emitter. The framed reference-SNR projection groups those paths by exact
(frame, group, emitter index, receiver) identity and sums their complex voltages
before measuring power; constructive and cancelling paths are therefore not
misreported as a sum of powers. Persistence, annotation, and inspection consumers
are migrated in subsequent bounded checkpoints; this producer seam does not make
unframed and framed rows interchangeable.
Source variant evidence¶
source_provenance.variant_evidence is the public extension seam for catalogs
with realized sub-axes. Core owns a bounded ragged row contract containing a
key, canonical lexical value, declared value type (int64, float64, or
enum), and unit. The use case owns the closed key schema and allowed value
domains, and emits only applicable rows; missing facts are absent rows, never
numeric or string sentinels.
Schema keys, entry keys, and allowed values use canonical sorted order. Entries
are unique, every entry must be declared, and every value must belong to its
key’s domain. Duplicate, contradictory, noncanonical, out-of-schema, and
out-of-domain structures fail configuration validation. The schema fixes the
graph type while realized entries remain parameter-identity and provenance
bearing. Omitting the extension preserves the pre-extension parameter
serialization and graph identity. SDS stores the rows under
<provenance>.variant_evidence.{offsets,key,value,value_type,unit} with shared
offsets and enum catalogs; readers do not infer realized truth from config.
The authoring models are VariantEvidenceField, VariantEvidenceEntry, and
VariantEvidenceParams; variant_evidence_type returns their typed graph
value. They are exported from rfgen.nodes.plan.provenance and
rfgen.nodes.plan, while the three authoring models are also available from
rfgen.nodes.
Signal Atlas transmission and receiver boundaries¶
Signal execution binds the approved descriptor IDs through
SIGNAL_OPERATOR_IMPLEMENTATIONS and resolve_signal_operator without changing
the graph topology. SymbolSource/UniformBitSymbolSource and
Modulator/ConstellationModulator are the public source seams;
QPSKModulator is the closed QPSK member, while QPSK and 16QAM
use explicit normalized constellations. oscillator_impairment owns CFO, linear
drift, and a supplied phase-noise process once. Existing CarrierTranslation
then precedes TimePlacement; sionna_port_boundary is a checked boundary, not
another propagation model. Radar uses fmcw_spec, fmcw_synthesize,
reference_tap, and dechirp. frontend_boundary is radar-only.
RegisteredSignalProviderCatalog selects a sole default or an explicit
ProviderRegistration beneath each unchanged logical graph operator. It rejects
missing, ambiguous, wrong-role, and capability-mismatched selections before a
draw. RoleKind, SourceProviderParams, ModulatorProviderParams, and
FadingProviderParams close the role-specific Params. ProviderProvenance
records normalized distribution/version, entry-point coordinates, typed ports,
stateless lifecycle, offered/required/resolved capabilities, runtime/library
version, resolved Params, and the explicit or sole_default reason.
Installed extensions publish one class under the stable
rfgen.signal_providers entry-point group, exposed as
SIGNAL_PROVIDER_ENTRY_POINT_GROUP. Its provider-declared immutable
SignalProviderDescriptor supplies RoleKind, logical operators,
SignalProviderPortSpec inputs/outputs, Params-schema version, capabilities, lifecycle,
and thread safety. from_environment loads and normalizes distribution metadata
once for stock generation, rejects malformed descriptors fail-closed, and keeps
source-tree builtins as deterministic defaults. Optional closed job fields
providers.symbol_source, providers.modulator, and providers.fading select
an installed identity without adding blocks to the owner YAML when defaults are
desired. Publication workflows may require installed provenance explicitly.
ConstellationParams closes constellation authoring; fmcw_schedule and
schedule_set_join bind the corresponding planning nodes.
fmcw_resample_to_acquisition uses per-chirp polyphase resampling to preserve
the authored chirp duration on the capture clock. ResamplingEvidence records
the exact source/target rates, rational factors, sample counts and boundary.
DeclaredPDP contains deterministic delay/power intent only. Stochastic draws
belong to the FadingProvider seam; IndependentRayleighFading is the bounded
independent-Rayleigh implementation. TimeIndexedCIR is the public
carrier-phased [snapshot,rx,tx,path] value with absolute delays, validity,
clock, native-solve authority and conversion-phase authority. Its rx/tx axes
are stable (native endpoint index, antenna index) channel identities, so
SIMO/MIMO coefficients are not collapsed into the path axis.
transfers_to_cir exposes frozen RT transfers without another carrier phase;
cir_to_cfr evaluates exactly
H(delta_f)=sum_path h*exp(-j*2*pi*delta_f*tau) over valid paths. Carrier-free
or mismatched-carrier CIR is refused.
StagedExecutionFrame is the graph-authoritative dispatcher: it walks the
immutable compiled stage sequence once, checks every descriptor identity and
input/output reference, resolves implemented operator identities, and records
ExecutionTraceEntry rows. SignalAtlasGraphRuntime supplies the bound
transmission, propagation, receiver, radar-processing, and publication
handlers. resolve_operator_implementation consults those concept-owned
catalogs without importing a use-case package. A missing, reordered,
duplicated, substituted, or unbound node fails at its exact graph identity.
FirstUnavailableNode carries that exact stop and completed trace.
ProcessAuthority, SignalWave, and SourceBits are immutable in-memory
execution values; emitter_id and signal_compatibility derive their semantic
identity and checked frame. port_frames, radar_specification, and
relationship_identity derive exact graph-bound axes and identities.
path_interaction_evidence exposes the converter’s
native evidence without recomputation.
CommunicationsParameters, RadarParameters, FMCWScheduleParameters, and
ArrayLayoutParameters are structural executor views, not authoring schemas.
The installed pipeline owns and validates its complete Pydantic parameter
model; decode_pipeline_parameters resolves that exact entry point and refuses
non-model output before core execution consumes the structural view. This keeps
core independent of Signal Atlas use-case packages while retaining strict
schema authority.
Transmission now fills the authored capture grid. Communications independently
draws and modulates all three attributed emitters, then applies transmitter
oscillator, Rapp PA, carrier ownership, time placement and TX-port mapping in
graph order. Transmitter oscillator phase is evaluated on absolute acquisition
times (start_s + n/fs - phase_origin_s), and its retry-stable phase-noise draw
uses canonical acquisition indices rather than burst-local indices. Radar does
the same for both FMCW radars. Its reference tap copies
the actual observed placed waveform after oscillator and PA processing; it
never regenerates a pristine chirp. calibrated_dechirp converts that tap to a
unit-magnitude dimensionless phase reference before mixing, so PA amplitude
does not spuriously multiply the voltage observation; near-zero samples inside
active reference support fail. RadarReferenceMixerEvidence records tap point,
normalization, support, minimum magnitude and calibrated mixer gain.
The receiver chain is fixed: coherent incident join, one system_noise draw,
matched_power_wave_termination, visible oscillator_downconvert, if_filter,
gain_compression, optional
communications iq_impairment, then separate adc_scale, adc_clip, and
adc_round_code. Input white complex noise has sample variance k*T*fs, split
equally across I/Q, and is drawn once after the coherent join. The causal
linear-phase scipy.signal.firwin/lfilter IF filter then realizes
ENBW=fs*sum(|h|^2)/|sum(h)|^2; zero initial state prevents circular wrap.
ReceiverNoiseParams, NoiseEvidence, FilterParams, and FilterEvidence
retain the 290 K reference, input PSD/sample covariance, predicted filtered
covariance, requested/realized ENBW, taps, group delay, boundary convention,
and draw count.
design_if_filter produces that exact qualified filter before the noise draw,
so prediction and application use identical taps.
Receiver CFO, drift and phase noise therefore rotate terminated voltage and
proper thermal noise before filtering; the unit-magnitude rotation preserves
noise covariance. NoisyIncidentWave remains a power wave. VoltageWave begins only at matched
termination, where positive real R0=Rload and Vrms=sqrt(R0)*a are enforced.
GainCompressionParams and IQImpairmentParams keep their phenomena separate.
ADCParams, ADCScaled, and ADCClipped preserve float64 V/code conversion,
signed-int16 clipping, saturation count, and ties-to-even rounding.
Stock generation executes each transmission and receiver operator once.
Communications proceeds directly to its record projection; radar first computes
the declared auxiliary products and then proceeds to its record projection.
Neither path publishes a partial record. record_process_seed derives the
retry-stable per-record noise stream. ProcessSamplingEvidence,
OscillatorProcessEvidence, ReceiverChainEvidence, ADCConversionEvidence,
and ReceiverChainResult
retain complete process keys/seeds, ordered lineage, noise/filter/termination,
gain/IQ, ADC scaling/saturation/rounding, radar-dechirp state and ADC codes.
SourceSymbolAuthority retains each emitter’s source provider identity/version,
runtime, complete sampling key/seed/draw shape, bit mapping and operation
identity; SourceSymbolProcessEvidence is its frozen receiver-chain evidence
projection. SignalProviderCatalog is the public execution selection seam and
RegisteredSignalProviderCatalog resolves and directly invokes stock or
third-party source/modulator implementations; draw_fading makes the fading
seam executable under the same authority. ProviderSelectionError,
ResolvedProvider, and core_registration complete that public contract.
build_provider_descriptor is the helper used by builtins and conforming
third-party classes; providers must declare the descriptor themselves.
Binary source conformance is semantic: exact requested rank-one count, uint8
encoding, and every value in {0,1}; bool, alternate integer codes, and shape
drift fail. Provenance retains the MSB-first constellation-label mapping and
binary_uint8 encoding. Modulator Params declare bits per symbol, exact-divisible
framing, rank-one output, and unit-constellation-average-power normalization.
The returned symbol count must be exact, complex and finite, with finite
strictly positive mean power. The PA repeats the finite-positive-power guard
before normalization, so invalid plugins cannot cause divide warnings or NaNs.
Fading output must be finite and exactly aligned with the declared PDP path
axis; failed outputs never enter retained provider provenance.
OscillatorDownconvertParams and
RadarReferenceMixerParams are strict, immutable operator parameters.
apply_receiver_oscillator and mix_actual_radar_reference are the focused
runtime bindings; amplify_wave, append_wave_lineage, draw_source_bits, and
modulate_bits bind the corresponding
transmission operations without hiding graph nodes; validate_provider_ports
checks their declared ports against the compiled NodeDescriptor.
record_process_evidence constructs the authority and
execute_receiver_chain remains the direct typed receiver-chain entry point;
radar callers must supply the graph-bound actual reference tap.
The production scene binder uses public load_scene, AntennaArray,
Transmitter, and Receiver APIs. It sets the authored carrier, exact element
coordinates, endpoint poses and velocities at the solve epoch, and verifies
authored object/material/native-ID bindings. Missing assets and unsupported
trajectory evolution fail with typed availability/compatibility errors.
Propagation seeds are Philox uint32 values derived from the persisted full
bundle-scoped sampling key, never record ordinal. A bounded thread-safe cohort
coordinator caches immutable results by exact path-affecting identity; retries
and record fanout reuse one solve, while any scene/material/carrier/array/epoch,
geometry, solver, or propagation-key change forces another solve. Closing the
coordinator clears its bounded cache.
Channel-response seam¶
Propagation solvers no longer own waveform power scaling or CIR application.
scene_facts is the authority for the finite capture extent: its PLAN-staged
sample_grid output is a float64 CaptureSampleTimes[time=N] coordinate tensor
whose GridRef states clock, spacing, origin, and seconds units. Time placement
and every CIR producer bind that one value. A producer sizes a response without
receiving IQ. Graph binding rejects a producer capture extent with the wrong
sample count, and CIR application rejects a different clock, spacing, or
origin (plus the wrong epoch count for a dynamic CIR). The coordinate payload
must exactly equal
origin + arange(N) * spacing; non-finite or contradictory coordinates are
refused.
The public v1 channel-response contract is deliberately one link and one
TX/RX stream: channel_impulse_response_type() is a one-row ragged value whose
columns are delay_s: float64[path] and dimensionless
gain: complex64[path, cir_time]. Invalid backend padding is removed by the
producer’s authoritative validity mask; retained paths keep backend order and
are neither sorted nor aggregated. cir_facts_type() carries the matching
time offsets, carrier, discrete tap bounds, delay reference, coefficient
reference, and the fixed normalization="none" statement. Its
application_grid field is the full concrete CaptureSampleTimes[time=N]
leaf value the producer consumed, including coordinates and the leaf tensor’s
exact GridRef. That leaf is the persisted authority: unlike a qualifier on
the parent struct, its grid participates in canonical type bytes and SDS writes
its coordinate metadata. The retained parent qualifier must agree with the
leaf but is not an independent source of truth. Every valid tap window contains
zero: l_min <= 0 <= l_max.
The graph-facing operations are separate:
declared_power_delay_profile(DeclaredPowerDelayProfile) publishes one deterministic analytic PDP and its RMS-delay fact atWhen.PLAN.pdp_rayleigh_fading(PDPRayleighFading) consumes that PDP plus the plan’s capture grid and carrier, and produces one static seeded CIR. It consumes no waveform and embeds no transmit power.the Sionna statistical and RT solver selectors likewise publish
cirandcir_facts; all eight are ordinaryTransformimplementations with a PLAN-stagedcapture_gridinput, notWaveformTransformimplementations. They do not receive or apply IQ.apply_cir(ApplyCIR) consumes an independently power-scaled complex64 volts waveform plus the two outputs from one CIR producer. It uses the producer’s exact tap window with Sionna’s unnormalised time-channel conversion. It constructs the backend’s full convolution window internally and maps retained output epochnto coefficient epochn, including whenl_minis negative. A zero-path CIR is represented by an empty ragged row, canonical(0, 0)bounds, and an all-zero output.cir_to_cfr(CIRToCFR) evaluates the oracle continuous-delay response at a caller-authored, ordered finite tensor of signed baseband frequency offsets. It does not FFT, sort, shift, normalise, or claim to model hardware CSI.
Every registered propagation-effect producer implements the public
PropagationProducer marker in addition to Transform. It declares a typed
PropagationConditioning member and consumes PropagationEndpointFacts on an
endpoints input at When.PLAN. Registration refuses an external producer
that omits the marker contract or PLAN endpoint dependency, and the contract
probe verifies the bound endpoint value type. context_only models use this
fact for authoritative link identity but their output bytes are invariant to
endpoint-pose perturbations under a common random draw.
geometry_conditioned models must causally use the endpoint pose. ApplyCIR
is an applicator and deliberately does not implement the producer marker.
The authoritative endpoint public API consists of
PropagationEndpointFactsPlan and PropagationEndpointFactsParams, the
propagation_endpoint_facts_type factory,
require_propagation_endpoint_facts_type validator, and
propagation_endpoint_poses payload projection. Producer facts use
propagation_conditioning_type for the closed conditioning enum.
ENDPOINT_FACTS_INPUT is the shared geometry-ground-truth port name.
receiver_entity_ref_type and transmitter_entity_ref_type construct the
identity projections joined by the endpoint plan.
Geometry-free association is a separate structural variant, not a posed
endpoint with missing coordinates. PropagationLinkFactsPlan and
PropagationLinkFactsParams consume the exact transmitter and receiver
EntityRef values and publish PropagationLinkFacts with
spatial_scope = "not_modeled"; the type contains no frame or pose field.
propagation_link_facts_type constructs that value,
require_propagation_link_facts_type validates only the pose-free variant, and
require_propagation_authority_facts_type accepts the exact union of that
variant and posed PropagationEndpointFacts. Geometry-, kinematics-,
pathloss-, and RT-conditioned operators still require the posed refinement.
EndpointSetupProducer is the public structural ownership marker for either
endpoint. A conforming PLAN implementation publishes matching complete setup
facts and EntityRef from one producer; an identity-only plugin is detached and
cannot establish authority. A pose may be absent for an explicitly unplaced
entity, but when present it must be owned and emitted by that same producer.
The common materialization validator compares the outputs with the cited
inputs, so this extension seam applies equally to core and third-party setup
producers. If a transmitter identity is framed, the producer must consume
exactly one PLAN population allocation, cite it with one
EntityGroupAllocationRequirement, declare a strict local ordinal, and emit
facts and identity whose canonical allocation, frame catalog, group, and
index_base + ordinal index all agree. A framed no-input setup or a copied,
foreign, or uncited allocation therefore cannot establish endpoint authority.
deterministic_identity_transport is the bounded context-only use of pose-free
association. DeterministicIdentityTransport and
DeterministicIdentityTransportParams pass a clean complex-voltage signal
bit-for-bit on the same sample grid while publishing facts built by
deterministic_identity_transport_facts_type. Those facts state
transfer_model = "deterministic_identity",
geometry_disposition = "not_modeled", and the exact link reference. The node
does not claim attenuation, delay, Doppler, range, line of sight, or a scene
frame.
Receiver-incident multi-link aggregation¶
Multi-emitter receiver custody is expressed as atomic rows, never as separate
signal and link collections. ReceiverIncidentContribution with
ReceiverIncidentContributionParams binds one identity-transport signal, its
same-producer facts, the exact link facts, and placement/support evidence. The
row keeps waveform and evidence inseparable. ReceiverIncidentAggregate with
ReceiverIncidentAggregateParams consumes one ragged collection, requires
unique contribution and directed-link identities terminating at one receiver,
sorts by (emitter_group_id, emitter_index, link_id, contribution_id) when
framed (and the legacy key without emitter_group_id when unframed), and
performs the floating complex-voltage sum in that same order. A legal zero-realization collection
emits zero clean IQ, [0, 0] evidence/custody offsets, and no phantom member;
the aggregate boundary can then establish one receiver-input plane for one
subsequent noise realization. ReceiverIncidentContributionProducer marks the
checked atomic seam, and require_receiver_incident_contribution_type accepts
only the exact legacy row or its complete canonical framed variant.
PropagationAggregateProducer marks the checked aggregate seam.
The aggregate publishes one summed signal, persisted ordered evidence, and an
internal custody value containing the exact member waveforms. Persisting the
ordinary aggregate facts does not copy those member waveforms: SDS stores one
aggregate IQ tensor plus the ordered ids, support, receiver, count, grid,
linear_complex_voltage_sum, context_only, and not_modeled evidence. The
internal waveform custody exists only for exact runtime membership checks.
ReceiverIncidentAggregateBoundary is the checked transition for that one
aggregate. Its public marker is ReceiverIncidentAggregateBoundaryProducer;
it requires signal, facts, and custody from the same
PropagationAggregateProducer, cites one receiver measurement, and emits one
receiver-input signal plus the exact ordered relation. The shared helper
receiver_boundary_association_fields returns either a single-link reference
or the aggregate count/ordered rows without pretending the two variants have
the same shape.
ReceiverIncidentMember is an optional exact-member projection guarded by
ReceiverIncidentMemberProducer. It proves that one atomic contribution is in
the cited aggregate custody and exposes that member at the already-established
receiver plane; it does not create a second boundary or noise draw.
For variable cardinality, ReceiverIncidentReferenceSnr and
ReceiverIncidentReferenceSnrParams consume the exact original contribution
collection, aggregate boundary facts/custody, and one finite positive
receiver-input variance. The checked ReceiverIncidentSnrProducer contract and
its graph-side ReceiverIncidentSnrRequirement require the same collection
origin and exact boundary/noise lineage. The implementation helper
receiver_incident_reference_snr_values emits ordered ragged scalar rows with
shared denominator and explicit defined status; it emits no waveform. A
collection whose contribution schema omits support_semantics retains the
exact active_support_mean_square_voltage reduction. A collection carrying the
exact nominal_sample_cell_interval singleton instead reports
total_signal_energy_over_nominal_sample_cells: contributions for one framed
identity are summed samplewise as complex voltage, their full finite-vector
energy (including filter tails) is divided by the integer cardinality of the
union of their half-open nominal cell intervals, and overlaps count once. This
is an energy-equivalent full-noise-band SNR; it does not assert that the
contributions are synchronized sources or that the denominator is
occupied-bandwidth noise. The legacy row remains the exact six-field
contribution projection. A framed row is a distinct exact schema containing the
full canonical flattened frame projection, global emitter index, and receiver
identity. Framed and unframed contributions cannot mix. Multiple contributions
for one framed physical emitter are coherently summed as complex voltage before
one row is measured, while a zero-cardinality realization emits no identity row
and retains the reference and undefined-policy evidence. These are reference
SNRs for contained emitters, not SINR claims about separable signals after
superposition.
The general EmitterSnrLabel has a clean framed alternative without changing
its legacy four-column output. A framed row binds identity_input to exact
realized ReceiverIncidentMemberFacts; EmitterSnrIdentityRequirement makes
the graph resolve that identity, every numerator signal, and every paired
placement fact to the same checked member producer through container aliases.
The label copies the member’s canonical frame projection, global emitter
index, and receiver identity into one flat ragged rows value and orders rows
by (group, emitter, receiver). It does not accept an allocation or reconstruct
a catalog. Framed and unframed rows cannot mix, and duplicate physical identity
rows refuse because this label has no waveform collection from which it could
coherently combine them. The public EmitterSnrProducer marker is fenced at
registration and execution: marked extensions use the exact legacy or framed
schema, checked inputs are isolated from plugin mutation, and scalar/identity
outputs are recomputed from pristine custody before publication. Registration
derives the ordered EmitterSnrIdentityRequirement tuple independently from
the canonical row parameters, so a plugin cannot omit or elect different
custody metadata. Materialization also detaches those authoritative parameters
from plugin-owned state before RUN. Framed rows for the same checked receiver
must cite one exact boundary and one exact receiver-input variance producer;
the label-wide placement schema chooses exactly one power convention. Mixed
active and nominal rows refuse. Nominal rows use the same full-energy-over-union
cell reduction; active rows retain the existing active-support
mean-square-voltage ratio. Neither is an occupied-bandwidth or ENBW-normalized
estimand.
DetectionGroundTruthProducer is the checked extension seam for D3a detection
evidence. The exact legacy DetectionGroundTruthParams variant has no frame or
identity inputs and retains its historical columns and serialized parameters.
The framed variant names one frame_input and one identity_input per event;
DetectionGroundTruthRequirement makes the graph resolve the frame to the one
checked EntityGroupFrameProducer and each identity/provenance pair to the
same checked transmitter setup through select, conditional, repeat, and
subgraph aliases. Each row in that requirement is a
DetectionIdentityRequirement. The label derives group code and global emitter index from
that identity, verifies the flattened provenance projection, and republishes
the canonical frame once. It cannot author a private group catalog or default
a missing framed row to group zero; one scene cannot mix framed and legacy
detection producers. Checked inputs and parameters are detached
from plugin-owned state, and outputs are recomputed from pristine custody before
publication. Multiple events for one emitter remain legal when their event
evidence differs; exact duplicate realized events refuse.
Detection class evidence has one typed precedence rule. If a row’s
SourceProvenanceFacts declares the optional class_name enum, that PLAN fact
is authoritative and must be drawn from the label’s exact class_names
catalog at class_id. This supports dataset taxonomies finer than a reusable
source implementation’s own class fact. If the provenance type omits
class_name, the label preserves the legacy check against the realized
source’s self-reported class. Payload-only fields cannot opt into the new path,
and a type-declared provenance class missing at runtime refuses rather than
falling back. Source evidence remains mandatory for family and event custody.
Repeat ordinals select string identities only through an explicit ordered
catalog. resolve_indexed_vocabulary_member resolves the bounded integer
before model validation; link_id_index and contribution_index are excluded
authoring helpers, while the resolved ordinary strings and exact enum catalogs
enter the bound types, serialization, and identity. Direct-string authoring has
the same resulting bytes. Interpolation or an out-of-range, duplicate, empty,
or mismatched catalog is refused.
CORE_PROPAGATION_PRODUCER_TYPES is the marker-filtered subset of
CORE_TRANSFORM_TYPES. transform_plugins returns the open entry-point
registry with core registrations and the lazy external producer fence applied.
The public extension markers are PropagationProducer for a first propagation
solver and PropagationEffect for a propagation transform that also consumes
upstream IQ and must reconcile inherited endpoint ancestry.
DeterministicIdentityTransportProducer is the narrower public marker for a
pose-free, context-only propagation effect: its signal output must be
bit-identical to its input and its exact
DeterministicIdentityTransportFacts output must cite the same link. The
catalog checks the canonical ports and the execution boundary checks those
values, so subclassing the marker cannot relabel attenuation or delay as an
identity transfer. PlacementEvidenceProducer is the corresponding public
support-custody marker. An origin such as scene_placement authors one
scope-qualified signal/facts authority; a refinement names its exact
placement_authority_input. Every bound transform whose concrete NodeSpec
consumes and publishes realized complex voltage declares the closed
PlacementEffectDisposition, including plugins that subclass Transform
directly rather than WaveformTransform: transparent is the strong claim that
time support, carrier, frequency offset, bandwidth, and grid all remain true;
evidence_transforming consumes the exact paired facts and publishes a newly
checked signal/facts authority; invalidating deliberately drops placement
authority. Unclassified plugins cannot register. carrier_frequency_offset
is evidence-transforming when placement facts are bound: it increments both
frequency_offset_hz and carrier_hz while retaining time support, bandwidth,
semantics, and grid. The corrected occupied support must remain inside complex
Nyquist and the corrected absolute carrier must stay positive. Its raw
pre-placement mode remains legal and publishes no placement facts. Resampling,
filtering, arbitrary per-sample phase rotation,
nonlinear amplification/limiting, oscillator/mixer effects, and noise are not
claimed transparent. Crossed or stale signal/support facts fail before
generation even when their grids and shapes match.
RUN and MATERIALIZE call the public recursive validate_value_payload helper
to validate every payload against its declared value type before shared storage
or SDS encoding. A tensor payload must be a concrete strided torch.Tensor
with exact dtype, rank, and concrete axis sizes; ragged tensors additionally
require monotone, in-bounds offsets whose segments each match the declared item
tensor. Identity transport additionally compares dtype, device, shape, and raw
tensor bytes, so dtype widening and signed-zero changes cannot pass as an
unchanged transfer merely because torch.equal considers the values equal.
For time-varying CIRs, cir_time=N: coefficient column n is the channel at
retained capture epoch n, and its cadence must exactly equal the application
grid. Static and zero-path CIRs use cir_time=1 but still carry the full
application_grid of length N; a one-epoch response does not erase the
capture extent it is valid for. ApplyCIR alone creates any internal
N + L - 1 padding required by full convolution; that padding is not part of
the public CIR. No consumer interpolates or regenerates published coefficients.
The reusable value factories dominant_path_delay_type and
cir_delay_reference_type keep scalar summaries structurally distinct from
the established per-path PathDelay tensor. DominantPathDelay means the
delay coordinate at the first maximum-|gain| coefficient; the closed reference
is absolute_propagation or first_arrival_excess. The common materialization
hook calls validate_dominant_path_delay_facts, which requires those exact
types plus a finite nonnegative payload and a valid reference before a record
can be published.
Ray tracing makes path absence explicit in its retained facts.
dominant_path_status="absent" is valid only with num_paths=0, canonical
(0, 0) bounds, and zero finite scalar placeholders. Those delay/gain zeros
are non-physical placeholders required by the persistable scalar algebra; they
must not be read as a zero-delay path. "present" requires a positive path
count. validate_ray_tracing_path_facts enforces those conditional semantics.
The former graph selector sionna_cir_dataset is removed. Its Python class is
compatibility-only and unregistered. Author the equivalent custom-PDP model as
declared_power_delay_profile → pdp_rayleigh_fading → apply_cir; this makes the
declared PDP, stochastic realization, transmit-power scaling, and application
independently visible and reusable.
The public construction and validation spellings are
DeclaredPowerDelayProfileParams, declared_power_delay_profile_type,
declared_power_delay_profile_facts_type,
validate_declared_power_delay_profile, PDPRayleighFadingParams,
ApplyCIRParams, CIRToCFRParams, channel_frequency_response_type,
capture_sample_grid_type, require_capture_sample_grid_type,
validate_capture_sample_grid, single_link_cir,
validate_channel_impulse_response, and validate_ray_tracing_path_facts. These names are
owned by the plan, propagation-transform, and value-contract modules alongside
the classes and type factories described above.
Receiver-input evidence and noise¶
receiver_measurement publishes identity, measurement_plane = "receiver_input", nominal receiver bandwidth, explicitly authored effective
noise bandwidth (ENBW), noise figure, and impedance. Nominal bandwidth and
ENBW are distinct fields; thermal noise uses only ENBW. This is a declared
receiver model, not evidence that a physical filter was measured.
The plan-side construction surface is ReceiverMeasurementPlan,
ReceiverMeasurementParams, and receiver_measurement_facts_type.
receiver_input_boundary consumes the final propagation signal and its facts,
the receiver measurement, and the same link’s PropagationEndpointFacts. It
passes the complex64 volts payload unchanged while adding the
receiver_input qualifier and publishing ReceiverInputBoundaryFacts. Those
facts include the exact CaptureSampleTimes application-grid leaf, receiver
and link identity, plane, both bandwidth fields, noise figure, and impedance.
Raw emission cannot be relabelled as receiver input because it lacks final
propagation facts. The boundary itself accepts only an unqualified input;
receiver_input is established there exactly once rather than trusted from an
upstream producer.
PropagationLinkRef is the pose-invariant causal identity shared by endpoint,
channel, CIR-application, pathloss, and receiver-boundary facts. Both structural
variants contain the link id and singleton transmitter/receiver identities. A
posed endpoint reference contains frame = "scene"; a pose-free association
reference instead contains spatial_scope = "not_modeled", never both.
Changing a posed endpoint’s pose leaves its reference type unchanged; changing
any identity makes the types incompatible at graph binding. The boundary also checks the payload
echo and requires its signal and upstream facts from one immediate producer.
propagation_link_ref_type(endpoints_type) returns the structurally specialized
link-reference type nested in one endpoint type;
propagation_link_ref_payload(endpoints_payload, endpoints_type=...) validates
the payload echo and returns its link_id, tx_id, rx_id, and frame.
These are public exports from rfgen.nodes.plan. They establish link identity,
not pose ancestry: PropagationAncestry marks the checked producer,
application, effect, and boundary transitions, while graph dataflow carries the
resulting authority set transparently through ordinary transforms, combiners,
and container aliases. Thus two endpoint producers with identical ids but
different poses cannot be interchanged midway through propagation, and a mixed
set cannot enter the single-link receiver boundary.
The public Python spellings are ReceiverInputBoundary,
ReceiverInputBoundaryParams, ReceiverInputBoundaryProducer, and
require_receiver_input_boundary_facts_type.
The two noise selectors are intentionally different:
awgnis synthetic target-SNR noise. It computes full-input mean-square voltage, refuses zero or power at/belowminimum_signal_power_v2, and adds complex Gaussian noise at the requested ratio. That parameter is the sole numerical floor; the kernel has no hidden clamp.receiver_thermal_noisecomputesk T B Ffrom boundary ENBW, reference temperature, receiver noise figure, and impedance, and publishes both watts and volts-squared variance. It does not accept an SNR target.
The registered implementation is ReceiverThermalNoise; its authored
temperature/floor model is ThermalNoiseParams.
Both registered selectors accept only one [time] receiver tensor. Rank-zero,
rank-two, or heterogeneous receiver attribution is refused until an explicit
per-receiver evidence contract exists. A contradictory measurement-plane
qualifier is also refused. Both require an exact receiver_input qualifier;
typed facts alone cannot forge that transition. Supported target SNR is
[-300, 300] dB and supported receiver noise figure is [0, 300] dB. Extreme
or non-finite parameters and non-finite derived variances are refused before a
random draw.
Noise nodes cite the exact receiver_input_boundary producer through graph
metadata, not a selector or node-name convention. emitter_snr requires each
row’s numerator signal, measurement facts, and denominator noise evidence to
resolve to that same boundary. Pairing pre-loss noise with a post-loss
numerator therefore fails validation even when the scalar types match.
Receiver ordering is enforced across flat graphs, repeats, and subgraphs: equivalent-input noise precedes receiver analog processing, which precedes the converter and receiver digital stages. A stage-less transform cannot conceal a backwards edge. Noise before propagation, AGC before equivalent-input noise, converter before analog processing, and a receiver analog limiter after the converter are refused. No relative AGC-versus-limiter order is imposed inside the analog interval.
The public limiter selectors are transmitter_crest_limiter and
receiver_voltage_limiter. Both take an absolute positive rail_v; neither
claims dBFS without converter full-scale evidence. The former is placed in the
transmitter plane and the latter in receiver analog processing.
Their Python spellings are TransmitterCrestLimiter,
ReceiverAnalogVoltageLimiter, and the shared VoltageLimiterParams.
The receiver-stage constants are RECEIVER_INPUT_BOUNDARY,
RECEIVER_EQUIVALENT_INPUT_NOISE, RECEIVER_ANALOG,
RECEIVER_CONVERTER, and RECEIVER_DIGITAL, in increasing order.
Radar-processing migration¶
The awgn public selector now requires signal and boundary_facts from one
receiver_input_boundary; it no longer accepts a raw signal-only binding.
receiver_thermal_noise has the same realized boundary inputs and no longer
takes a PLAN receiver_facts edge or an authored bandwidth parameter. Add a
receiver_measurement with effective_noise_bandwidth_hz, add the boundary
after the final propagation/pathloss application, and wire either noise node
from both boundary outputs. Wire emitter_snr’s numerator signal and
measurement input to that same boundary.
The six shipped record projections persist receiver_input.facts,
noise.facts, emitter_snr.measurement_plane, and
emitter_snr.power_convention. The nested facts retain link, receiver, grid,
noise, support semantics, and SNR power convention through codec and SDS
storage.
Direct-pose propagation plans, ambiguous magnitude limiting, and the prior
signal-only noise implementations are not public API: they are absent from
aggregate exports, entry-point registries, and selector catalogs. Replace
direct tx_pose/rx_pose propagation inputs with one
propagation_endpoint_facts PLAN edge, and replace an ambiguous limiter with
the transmitter or receiver selector appropriate to that instance.
Compatibility classes additionally declare the common non-registerable node
flag; the generic entry-point loader refuses an external package that points
any open role at one under a fresh selector.
Receiver conversion and IF stages¶
rfgen.nodes.transform.receiver.converter exposes four separately composable
converter effects. ConverterScaling and ConverterScalingParams keep
complex64 volts unchanged and publish a volts-per-code scale; a null full scale
is synthetic per-row normalization, not ADC calibration or AGC. Scaling also
publishes the signed maximum code, so resolution cannot disagree with a later
clipping stage through a second authored ENOB parameter.
ConverterRounding and ConverterRoundingParams apply ties-to-even to
the real and imaginary components without clipping. ConverterClipping and
ConverterClippingParams then clamp those components independently to the
signed code rails (not radially), while ConverterReconstruction and
ConverterReconstructionParams restore complex64 volts from the carried
scale. Each downstream stage requires its signal and scale from one immediate
producer, making the physical order part of the graph contract.
The former converter_quantization selector is removed. Its Python
ConverterQuantization class remains importable only for compatibility and is
not registered as a graph node. Replace one authored node with the ordered
converter_scaling → converter_rounding → converter_clipping →
converter_reconstruction chain.
rfgen.nodes.transform.receiver.if_chain exposes the corresponding separable
IF chain. IFFIRResponse and IFFIRResponseParams apply an odd-tap,
unit-gain causal FIR on the unchanged sampling grid. The impulse response begins
at the input origin; its envelope peak occurs at the integer group delay.
IFScalarGain and IFScalarGainParams apply a real voltage gain while
retaining exact alignment evidence. FIRGroupDelayRealignment and
FIRGroupDelayRealignmentParams shift the waveform left and zero-fill the
tail while retaining that same grid. This compensates waveform alignment with
declared tail loss; it does not change clock coordinates.
The former if_fir_filter selector is likewise removed; its IFFIRFilter
Python class remains compatibility-only and unregistered. Replace it with
if_fir_response → if_scalar_gain → fir_group_delay_realignment. Authors
who need the physical causal stream omit only the final alignment stage.
Resource nodes¶
A resource node answers “what world is this scene in?” and returns a name for it. It resolves at MATERIALIZE, like a plan node, and imposes no run ordering. The full contract, why the handle is a description rather than a live object, how the two authoring refusals work, and what is Sionna’s rather than rfgen’s , is in the architecture reference.
rfgen.nodes.resource exports ResourceNode, the role’s base class, which
is also exported from the module that defines it,
rfgen.nodes.resource.operator. A resource node subclasses it and implements
resolve(inputs, ctx), exactly as a plan node does; the distinction is what it
resolves. A plan node resolves a fact, a drawn carrier, a resolved pose. A
resource node resolves a handle: a name for a world that a realized node will
borrow and turn into a live backend object, so two consumers naming the same
world share one realization.
rfgen.nodes.resource.plugins exports resource_plugins(), the role’s
entry-point registry factory, and CORE_RESOURCE_TYPES, the selector-to-class
table core itself registers. Registration is open on the same terms as every
other role: an entry point in the rfgen.nodes.resource group names a
ResourceNode subclass, and the catalog constructs it through from_binding.
rfgen.nodes.resource.scene_world implements the one resource node core ships:
SceneWorld, assembles a base geometry plus the objects placed in it and emits a world handle on a singleworldoutput port atWhen.PLAN.SceneWorldParams, its parameters: abaseand a non-emptyobjectstuple. Also exposesstructure_id, the digest of everything about the world a record cannot change, which is what the ray tracer’s scene cache is keyed on.WorldBaseParams, the geometry the world is assembled on: kind, URI, content hash, and optional entrypoint. Validated at construction so a bad URI failsrfgen validaterather than the first record.WorldObjectParams, one physical object, declared as one nested entry: its mesh, its material and scattering coefficient, its scale, its pose and velocity, and thesamplingenvelope the biased ray launch needs to aim at it. The scattering coefficient is required and required to be non-zero, because zero is both Sionna’s ITU default and the value at which the object returns nothing at all to a monostatic receiver.world_type(params), returns theStructTypea world of those parameters emits. Each object becomes one nested field named by its ownobject_id, which is what makes an object individually addressable and what makes wiring to an object that no longer exists fail atrfgen validate. Each object’s sampling envelope is carried in the type rather than only in the payload, so the ray-budget refusal can run at validate time, where a consumer sees bound input types and no record exists yet.
Transform staging¶
A transform states, per dependency, whether it consumes a value that exists
after materialization (When.PLAN) or only after its producer runs
(When.REALIZED). The declaration is dependency_whens, keyed by dependency
name exactly like dependency_ports:
Class attribute |
Meaning |
|---|---|
|
dependencies a graph must bind |
|
dependencies a graph may bind |
|
dependency name -> port name, when they differ |
|
dependency name -> staging; default |
The default is When.REALIZED because that is what a signal, a filtered stream
or a placed capture is. A declared setup fact – a sample rate, a carrier, a
transmit power, a pose, a receiver’s facts struct – is When.PLAN and must say
so.
A dependency_whens key that names no declared dependency is rejected when the
class is created, so a typo cannot degrade quietly into the default.
The declaration is enforced at construction. Transform.from_params
compares input_whens against dependency_whens and refuses a binding that
disagrees, rather than echoing the graph’s tag into spec(). input_whens is a
required keyword with no default, precisely so the check cannot be switched
off by omitting it – a direct-construction test that forgets it gets a
TypeError, not silent non-enforcement. Pass {} to mean “no inputs bound”.
from_binding supplies it automatically, so a node reached through a
configuration is always checked.
Note that the fence lives in from_params. A node that overrides
from_binding outright – a supported thing to do – bypasses it and is
responsible for its own staging. A transform’s
staging is fixed by the mathematics it implements and is not the graph’s to
choose: awgn adds noise to samples, so its signal cannot be a plan fact.
Echoing would produce a node whose declaration no configuration can satisfy,
and, because When folds into node identity, a record id that silently depends
on how a wire was drawn.
What is compared is the when: the configuration declares on the input
port, not the producer’s own staging. The producer is checked separately, and
later, by the compiler’s edge check. A configuration can therefore have the wire
right and the when: wrong, which is the commonest form of this mistake.
A disagreement surfaces through the compiler as an AnalyzeError wrapping the
refusal, reproduced here in full:
AnalyzeError: cannot infer output types for node fading (selector
'sionna_rayleigh_block'): SionnaRayleighBlockFading consumes 'endpoints' at
When.PLAN, but the configuration declares that input at When.REALIZED. A
transform's staging is fixed by what it computes, not by the graph. Either set
'when: plan' on that input in the configuration, or, if this transform genuinely
consumes 'endpoints' at When.REALIZED, declare dependency_whens =
{'endpoints': When.REALIZED} on SionnaRayleighBlockFading. If the input is also
wired to a When.REALIZED producer, repoint it at a plan one. Declare its outputs
explicitly, or fix the parameters that stop it from binding.
It names three fixes, and they are the only three: change the when: on that
input, change the node’s dependency_whens, or repoint the wire. Which one is
right depends on where the mistake is, and the first is the usual answer.
The final sentence – “Declare its outputs explicitly, or fix the parameters” – belongs to the compiler’s generic wrapper, not to this check, and is not applicable to a staging disagreement. It is shown here because it is what a reader will actually see, and following it will not help.
Dependency names and port names¶
Four class attributes and one helper cross two vocabularies, and getting them the wrong way round is the easiest mistake to make here. The rule in one sentence:
dependency_names,dependency_portskeys anddependency_whenskeys are dependency names;dependency_portsvalues,port_when(...)and the port names inspec()are port names.
The constructor keywords the role synthesises cross the same line, which is the
part worth reading twice: _validated_ref_kwargs builds f"{name}_ref" from
the dependency name and f"{name}_type" from the port name. A
dependency whose port is renamed therefore appears in the constructor under two
spellings – below, tx_power_ref and tx_power_dbm_type.
A worked example¶
A gain transform is a compact example because it consumes a realized signal and one renamed plan fact at once.
A complete class that imports and constructs; not a verified end-to-end example.
from dataclasses import replace
import torch
from pydantic import BaseModel, ConfigDict
from rfgen.nodes.operator import NodeSpec, Port, Ref, ReproClass, When
from rfgen.nodes.transform.operator import WaveformTransform
from rfgen.nodes.values import ScalarType, TensorType, Value
class ExampleDeclaredGainParams(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
reference_loss_db: float = 30.0
class ExampleDeclaredGain(WaveformTransform):
"""Scale a waveform while citing a declared transmit-power fact."""
params_type = ExampleDeclaredGainParams
# `tx_power` is the dependency; `tx_power_dbm` is the port it binds to.
dependency_names = ("signal", "tx_power")
dependency_ports = {"tx_power": "tx_power_dbm"}
# Keyed by dependency name. `signal` is absent, so it keeps the
# `When.REALIZED` default; a key naming no declared dependency is rejected
# when the class is created.
dependency_whens = {
"tx_power": When.PLAN,
}
def __init__(
self,
params: ExampleDeclaredGainParams,
signal_type: TensorType,
signal_ref: Ref,
tx_power_ref: Ref, # `_ref` keywords are named for the DEPENDENCY
*,
tx_power_dbm_type: ScalarType | None = None, # `_type` for the PORT
) -> None:
self.params = params
self.signal_type = signal_type
self.output_type = replace(signal_type)
self.refs = (signal_ref, tx_power_ref)
self.input_types = {
"signal": signal_type,
"tx_power_dbm": tx_power_dbm_type,
}
def spec(self) -> NodeSpec:
return NodeSpec(
self.params,
tuple(
# `port_when` is keyed by PORT name and reads the declaration
# above, so what this node reports and what the role enforces
# cannot drift apart.
Port(name, self.input_types[name], self.port_when(name))
for name in ("signal", "tx_power_dbm")
),
(Port("signal", self.output_type, When.REALIZED),),
self.refs,
ReproClass.DETERMINISTIC,
)
def evaluate(
self, inputs: dict[str, Value], seed: torch.Generator
) -> dict[str, Value]:
del seed
scale = 10.0 ** (-self.params.reference_loss_db / 20.0)
return {"signal": Value(self.output_type, inputs["signal"].payload * scale)}
Note that input_types and refs are attributes this class assigns in its own
__init__. They are not provided by Node, Transform or WaveformTransform;
port_when and declared_port_whens are.
Core’s nine propagation models derive their staging from one shared set rather
than restating it; see rfgen.nodes.transform.propagation.staging, whose
plan_fact_whens(dependency_names) returns exactly the keys a given class
declares.
Outputs are a separate question and are not covered by dependency_whens. A
realized role’s outputs are produced by evaluate at run time, so they are
When.REALIZED regardless of what any input is tagged.
Scientific implementation origin¶
TorchSig
>=2.1.1,<2.2supplies constellation modulation throughtorchsig.signals.builders.constellation.constellation_modulator; see TorchSig documentation.Sionna
>=2.0.1,<2.1supplies NR PUSCH, OFDM, FEC, Rayleigh and 3GPP channel operators, plus ray tracing. RFGen composesPUSCHTransmitter,ResourceGrid,OFDMModulator, 5G encoders,ApplyTimeChannel, andPathSolver; see Sionna PHY and Sionna RT.Custom receiver impairments, placement transforms, and authored-metadata adapters are bounded node-local compositions. Their focused unit tests check identity, units, limiting cases, and deterministic keyed execution.
SigMF replay¶
SigMF replay under rfgen.nodes.source.replay.sigmf reads standard
core:sample_rate (samples/second), core:frequency (absolute RF center in
hertz), and core:license, plus RFGen extensions
rfgen:collection_authority, rfgen:occupied_low_hz, and
rfgen:occupied_high_hz. The occupied bounds are baseband-relative hertz and
must satisfy -sample_rate/2 <= low < high <= sample_rate/2; they are not
absolute RF frequencies. See the SigMF specification.
These authored/trusted values are pinned in replay identity. Production never
infers occupied bandwidth from IQ samples because doing so would replace source
provenance with estimator policy.
Contract probe for third-party nodes¶
rfgen.nodes.testing ships inside the wheel so an installed use-case package
can import it. It answers one question about a node the package registers: is
this a node any configuration can actually reach, and does it keep the
contract core’s own nodes are held to? Core’s in-tree spec-vs-declaration
invariant is scoped to core’s own registrations, so without this a third-party
node is checked by nothing.
The probe never accepts a hand-written binding. Build a ProducerScene of real
registered producers, name the wiring, and the probe reads every bound port’s
value type and When off the producer’s own spec():
import pytest
from rfgen.nodes import RoleKind
from rfgen.nodes.testing import ProducerScene, assert_node_contract, registered_nodes
DISTRIBUTION = "my-use-case" # the [project] name, not the import name
PARAMS = {"acceptance_channel": {"delay_spread_s": 1e-7}}
WIRING = {"acceptance_channel": {"signal": ("emitter", "signal")}}
VARIANTS = {"acceptance_channel": {"delay_spread_s": 3e-7}}
@pytest.fixture(scope="module")
def scene() -> ProducerScene:
built = ProducerScene()
built.add("emitter", RoleKind.SOURCE, "tone", {
"sample_rate_hz": 1e6, "duration_s": 1e-3, "clock_id": "scene",
})
return built
@pytest.mark.parametrize(
"node", registered_nodes(DISTRIBUTION), ids=lambda n: f"{n.role.value}:{n.selector}"
)
def test_every_registered_node_keeps_the_core_contract(node, scene) -> None:
report = assert_node_contract(
node,
scene=scene,
params=PARAMS[node.selector],
wiring=WIRING.get(node.selector),
variant_params=VARIANTS.get(node.selector),
)
assert not report.skips(), report.skips()
The final assertion is what turns “nothing failed” into “everything was
proved”. assert_node_contract already refuses a skip outside allow_skips,
so a blanket skip={check: "n/a" for check in ContractCheck} raises rather
than reporting nine skips and a pass; asserting on report.skips() narrows
that further to zero, which is what a package should aim for once it has
supplied variant_params and a scene whose producers all run.
registered_nodes raises UnknownDistributionError rather than returning an
empty list for a name it cannot find, because an empty parametrization is zero
coverage reported as a pass. Distribution names are matched PEP 503 normalized,
so my-use-case, my_use_case, and My-Use-Case are the same package.
What each check needs from you¶
Sources may declare input ports too. A source that reads a plan fact
(plan_dependency_names) is wired exactly like any other node, by name, from
a plan producer in the scene, and the role default refuses a realized wire
with an actionable message, which is a legal answer to the echo probe.
Check |
Needs |
Without it |
|---|---|---|
|
nothing beyond |
always run |
|
|
skipped |
|
every wired producer to have produced a value in the scene |
skipped |
|
the node to be a label declaring |
skipped |
ambient_isolation is reported separately from determinism on purpose. A
node can keep its ReproClass.DETERMINISTIC promise exactly, byte-identical
outputs on every run, while perturbing the process-global torch generator, so
that every peer scheduled after it depends on its position in the schedule.
Sionna and TorchSig both do this during setup, and core’s own sionna_cdl is
recorded as an example. Where the disturbance is inside a vendor library the
repair is not in your node, so acknowledge it with an explicit skip; because
the two checks are separate, doing so costs you nothing on
byte-reproducibility.
Warning
A passing ambient_isolation is weaker evidence than it looks, and the
weakness scales with your suite. A vendor backend disturbs the global
generator during a one-time lazy initialization, so within a single process
only the first probe to touch a given backend can observe it. Run the
harness across forty nodes that share one backend and you get forty green
ambient_isolation results that are, between them, at most one piece of
evidence about that backend, the other thirty-nine ran after it was already
warmed. The passing outcome’s detail says so at runtime; print
report.outcome(ContractCheck.AMBIENT_ISOLATION).detail if you want it in your
test output. To actually probe a backend, give it a test that reaches it first
in a fresh process (pytest --forked, a dedicated module, or a subprocess).
This bound is why the check runs before determinism rather than after.
Measured live: with the two checks in the reverse order, sionna_cdl reported
a clean ambient_isolation, because determinism’s two comparison runs had
already consumed the initialization. The ordering is enforced in
rfgen.nodes.testing.report.ORDERING_CONSTRAINTS and validated at import, so a
refactor that reorders the checks fails loudly instead of restoring that false
pass.
What a skip means¶
A skipped check proved nothing. Skips are recorded with a reason and never
counted as passes; read them with report.skips(), and assert that list is
empty where a suite intends to prove it opted out of nothing. Opt out
explicitly, for a stage that cannot execute without a heavy optional
backend, with a reason:
from rfgen.nodes.testing import ContractCheck
assert_node_contract(
node,
scene=scene,
params=params,
skip={ContractCheck.DETERMINISM: "evaluate needs the sionna path solver"},
)
An opt-out without a non-empty reason raises, and so does an opt-out on a check
outside allow_skips, which defaults to the checks that can legitimately lack
material (structural_kind, ambient_isolation, determinism, evidence,
output_conformance). Skipping construction, declaration,
producer_provenance, spec_agreement or when_staging is declining to be
checked rather than lacking material, so it requires widening allow_skips in
the same call, where it is visible in review. probe_node_contract is the
non-raising form, returning the same ContractReport for a suite that records
known gaps rather than failing on them.
assert_node_contract raises NodeContractError naming every failed check.
Two failures are worth recognising on sight. structural_kind reporting
“finding O1” means a value type’s kind is a function of a parameter value
rather than of the declared schema. when_staging reporting “finding O33”
means the node hard-codes a port’s When instead of echoing the wire it was
bound to, and the repair depends on the role. A label, combiner or allocator
echoes by naming input_whens in its __init__, which binding_evidence_kwargs
then supplies; that works today and needs no core change. A plan node cannot
echo a realized wire at all, so it must refuse one at construction. A transform
cannot echo either: Transform.from_binding passes refs and input_types,
neither of which carries When, so refusal is its answer until that role
default gains the channel. Note that structural_kind is deliberately weaker than
ValueTypeRegistry’s conflict rule: axis extents and a sample grid’s spacing
and origin are normalized away before comparison, so passing it means “no
params-derived vocabulary under a constant kind”, not “will register cleanly”.
Signal-chain ordering (rfgen.nodes.ordering)¶
Fine granularity makes a receiver front end substitutable, and it also lets a
configuration wire the quantizer before the mixer. Nodes therefore declare where
they act, and rfgen validate refuses a graph whose realized signal paths
contradict those declarations. See the architecture reference for the rule and
the YAML surface; this page names the API.
SignalPlane is the closed set of planes, TRANSMITTER, PROPAGATION,
RECEIVER, and PLANE_ORDER maps each to its position in the one direction a
signal travels, stated as data rather than inferred from enum declaration order.
A node declares two ClassVars, both defaulting to None:
Node.signal_plane: SignalPlane | None, which plane this node acts in.Nonemeans the node makes no claim: it is never refused and never refuses anything, which is what makes the mechanism additive.Node.plane_stage: int | None, position within that plane.Nonemeans the node has a plane but no fixed position inside it. Core exportsRECEIVER_ANALOGandRECEIVER_CONVERTERas the two stages it declares.
Neither folds into plan_identity: they constrain what a graph may say, not
what it computes, so declaring one moves no corpus.
ordering_violations(placed, plane_edges, stage_edges, exempt=()) -> tuple[OrderingViolation, ...] is the rule itself, over a mapping of
PlacedNode(name, plane, stage). It takes two contracted edge sets because
transparency is per-comparison: a node with no plane at all is transparent to
both, while a node with a plane and no stage is transparent only to the stage
comparison and stays an endpoint for the plane comparison. Each
OrderingViolation carries both endpoints and their planes and stages;
OrderingViolation.message() renders the refusal an author reads, naming both
nodes and the physics, and crosses_planes distinguishes the two cases.
A third-party node declares its own plane and is checked on identical terms ,
core holds no table of node names. The signal_plane check in
rfgen.nodes.testing refuses the two declarations that otherwise fail silently:
a stage with no plane, and a plane that is not a SignalPlane member.
Transmitter setup (rfgen.nodes.plan.transmitter)¶
TransmitterSetupPlan is a transmitter’s single owner: index, id, pose,
velocity and radiated power, validated by TransmitterSetupParams. It emits
facts (the struct transmitter_setup_type(params) returns), pose on the
shared TxPose contract, and tx_power_dbm on the shared TransmitPower
kind, three shapes of one declaration, so three different consumers need not
restate it. transmitter_id_vocabulary follows the rule in
rfgen.nodes.values.vocabulary: one kind, instance facts in the payload.
ReceiverSetupPlan mirrors it and gained an optional pose, emitting a pose
port only when one is declared. rfgen.nodes.plan.geometry exports the two
helpers both share, pose_payload builds the payload of a
geometry_pose_type value, and ORIGIN is the at-rest, unrotated default a
node means when it declares no orientation or velocity.
GeometryGroundTruthPlan cites those owners rather than restating them, over
plan edges named by POSE_INPUT and TX_POWER_INPUT. Each fact comes from
exactly one place, authored, or bound, and authoring beside a bound edge is
refused rather than silently resolved.
Line-of-sight state and path loss¶
Line-of-sight state is decided once per record, in the plan tier, and every node that needs it reads that decision rather than making its own. Path loss is a separate node that consumes the same decision.
Deciding the state: link_state¶
LinkStatePlan (in rfgen.nodes.plan.link_state, parameters LinkStateParams)
resolves one link’s line-of-sight state before any signal exists. It publishes
two ports: los, carrying the state itself, and facts, a struct built by
link_state_type holding the state, the probability it was drawn against, and
how it was decided.
There are exactly three ways to decide it, and a node uses exactly one:
|
meaning |
|---|---|
|
authored, for curriculum construction and reproducibility |
|
sampled from the scenario’s own TR 38.901 probability |
omitted |
read from the bound input port named by |
Authoring state beside a bound edge is refused rather than silently preferred.
Drawing requires a scenario and both endpoint poses, because the probability is
a function of horizontal separation and terminal height.
los_probability is that probability as a callable: it implements 3GPP TR 38.901
Table 7.4.2-1 directly rather than reading it off a Sionna scenario, so the plan
tier does not depend on an optional backend. It is pinned against Sionna across
54 scenario/distance/height combinations.
los_state_type is the shared two-member contract, the members are
LOS_STATE_MEMBERS: that every consumer of the decision binds, so a node cannot
be handed a state it cannot interpret.
Applying path loss: tr38901_pathloss¶
TR38901PathLoss (in rfgen.nodes.transform.propagation.pathloss, parameters
TR38901PathLossParams, both re-exported from rfgen.nodes.transform) applies
TR 38.901 basic path loss as a scalar amplitude scale, and publishes what it
applied on a struct built by pathloss_facts_type.
It is a node rather than a flag on the channel models because a toggle is not a substitution seam: a use case wanting a different path-loss law, a low-altitude drone link, a radar range equation, a LiDAR extinction model, can replace this node without forking the TR 38.901 large-scale-parameter machinery around it.
Both this node and the three system-level channel models require the los
input. That is deliberate and was learned the hard way: while it was optional
on the channel models, no configuration bound it, so path loss used the plan fact
while the channel drew its own state, and records carried two contradictory
line-of-sight labels. An optional input that carries a coherence constraint makes
the correct wiring possible and the incorrect wiring the default.
Deciding the indoor/outdoor state: indoor_state¶
IndoorStatePlan (in rfgen.nodes.plan.indoor_state, parameters
IndoorStateParams, both also re-exported from rfgen.nodes.plan and
rfgen.nodes) resolves one link’s indoor/outdoor terminal state before any
signal exists, the same shape link_state establishes for line-of-sight. It
publishes two ports: indoor, carrying the state itself, and facts, a
struct built by indoor_state_facts_type holding the state and how it was
decided.
There are exactly two ways to decide it, and a node uses exactly one:
|
meaning |
|---|---|
|
authored, for curriculum construction and reproducibility |
omitted |
read from the bound input port named by |
Authoring state beside a bound edge is refused rather than silently
preferred. There is deliberately no draw mode, but TR 38.901 is not silent
on the indoor fraction: Table 7.2-1 states an 80% indoor UT ratio for UMi/UMa
deployments, Table 7.2-3 gives 50% for RMa, and Clause 7.6.3.3 applies the
same LOS-probability procedure with the indoor ratio substituted. A
node-level draw was still declined because that ratio is a deployment-wide
calibration constant an author chooses, not a per-link function of geometry
the way LOS probability is, link_state’s draw takes a distance and a
height; TR 38.901 gives indoor UTs no per-link input to draw from, only a
flat ratio. Sionna’s set_topology also gives this node nothing to delegate
a draw to: los accepts None to mean “draw it,” but in_state has no such
sentinel and must always be supplied.
indoor_state_type is the shared two-member contract, the members are
INDOOR_STATE_MEMBERS, ordered ("outdoor", "indoor"): that every consumer
of the decision binds, so a node cannot be handed a state it cannot interpret.
A controlled indoor/outdoor mix uses the graph’s select primitive, not an
allocator, core ships no allocator role of its own. A kind: select node
with two branches (one authoring indoor_state’s state as indoor, one as
outdoor) and a draw.schedule.pattern naming each branch in the desired
proportion gives an exact, reproducible census: a five-entry pattern of four
indoor entries and one outdoor entry is an exact 80% indoor split over any
multiple of five records, matching Table 7.2-1’s ratio. draw.choices entries
must be unique (a plain, unscheduled draw selects uniformly among them); the
schedule’s pattern is where a repeated branch name encodes a ratio.
SionnaUMa and SionnaUMi require the indoor input; SionnaRMa does
not, and this is a scope boundary on o2i_model, not a claim that indoor
has no effect on RMa. UMa and UMi accept an o2i_model parameter selecting
between TR 38.901’s low- and high-loss outdoor-to-indoor penetration models.
RMa has no such choice: TR 38.901 Section 7.4.3.1 states only the low-loss
model applies to RMa, so Sionna’s RMa constructor takes no o2i_model
argument at all, even though it does apply that one fixed variant internally
whenever a terminal is indoor. indoor itself, the terminal state Sionna’s
topology needs, independent of any loss addend, has a real, measured effect
for every model family, RMa included; making RMa’s terminals indoor-expressible
is a separate, open capability gap, not something this fix closes or needed to.
Before indoor_state existed, the indoor/outdoor terminal state was
hard-coded outdoor everywhere, so no configuration could place a terminal
indoor at all. That is what this node fixes, and it is a real effect: an
indoor terminal is scored against an entirely different Sionna large-scale
parameter column (delay spread, angular spread, K-factor), which measurably
changes the delivered CIR and waveform energy, not merely a loss addend.
o2i_model itself still reaches no delivered record, on any configuration,
and this node does not close that. Sionna computes the O2I penetration-loss
addend only when the channel model’s own enable_pathloss is true, and that
is unreachable from any author parameter, SionnaUMaParams/SionnaUMiParams
declare no such field. Path loss on a shipped scene is applied by the
separate tr38901_pathloss node, which reads only basic_pathloss (see
below), not the O2I-inclusive sample_pathloss(). channel.facts now
reports pathloss_enabled next to shadow_fading_enabled so a consumer can
see this directly: whenever it reads False, nothing gated behind it ,
o2i_model included, reached that record. channel.facts also reports
indoor_state (the realized terminal state) alongside o2i_model (the
configured model choice), so a consumer can at least tell the two facts apart
rather than reading one unqualified name. Closing the o2i_model gap for
real needs tr38901_pathloss to stop reading basic_pathloss in favour of
sample_pathloss(), which is a distinct, larger change, filed separately
rather than attempted alongside making the indoor state itself real.
Scene realization transforms¶
CarrierTranslation with CarrierTranslationParams translates an emitter-local
waveform and publishes carrier_translation_facts_type. TimePlacement with
TimePlacementParams accepts that translated signal and its facts directly and
binds the authoritative PLAN-staged scene_facts.sample_grid,
publishes scene_waveform_type, and describes its decision with
time_placement_facts_type. PlacementEvidence with
PlacementEvidenceParams combines those decisions with source evidence into
the established placement-facts contract. Source and carrier facts are passed
through each hop. Their EmitterEvidencePairingRequirement is resolved by
graph authority through select, conditional, repeat, and subgraph aliases
rather than by comparing an immediate node name. A stale pre-resampling facts
wire is therefore refused even when its grid and shape happen to match.
Rational resampling and evidence custody¶
FHSS sources publish their randomized realized schedule as a
FrequencyHopEvents member of the same source-facts value that owns the IQ.
frequency_hop_events_type(event_count) constructs the exact typed struct:
parallel one-dimensional tensors carry channel index, source-frame center
frequency, half-open start/end sample intervals, and channel-allocation width,
plus the closed channel_allocation bandwidth semantic.
validate_frequency_hop_events(value_type, payload, channel_count=...) checks
that exact schema and, when a payload is supplied, tensor dtype and event-grid
shape, finite positive bandwidths, ordered non-overlapping intervals, and
channel bounds. It validates realized synthesis evidence; it does not attest a
protocol-defined hopping sequence or reinterpret channel allocation as a
measured occupied-power width.
DetectionEvidenceRowParams.event_input opts a row into one detection box per
event. It must name the same input as source_input; graph analysis also proves
that those facts are the emitter authority used to author the bound placement.
Cross-emitter source/event/placement wiring is therefore refused even when the
two sources have structurally identical facts. Rational source-evidence
resampling preserves channel and frequency values while correcting every
half-open sample interval onto the output grid.
Three selectors give the same rational polyphase kernel one unambiguous
physical stage. rational_resampler accepts only raw_receiver, is a
receiver-plane stage, publishes no facts, and deliberately invalidates
placement authority. Its public class is RationalResampler.
source_evidence_resampler uses SourceEvidenceResampler, accepts only
source_evidence, runs at TRANSMITTER_SOURCE_GRID before
TRANSMITTER_POWER/transmit_power, and publishes corrected EmitterFacts.
placed_evidence_resampler uses PlacedEvidenceResampler, accepts only
placed_evidence, and runs at TRANSMITTER_PLACED_GRID after placement but
before TRANSMITTER_POWER and propagation. It publishes corrected
ScenePlacementFacts. Reversing either resampler/power order is refused
because filtering a power-calibrated waveform can change its calibrated mean
power.
Every resampler also emits typed RationalResamplingCustody. It names the
requested and reduced rational ratio, native input and output grids and sample
counts, exact FIR tap count and normalized cutoff, separate anti-alias and
anti-imaging duties, nontrivial rate-conversion filtering, and nominal versus
residual group delay. Anti-alias is required only when the output rate is lower
than the input rate; interpolation by more than one requires anti-imaging. The implementation uses
design_polyphase_filter to create an immutable PolyphaseFilterDesign and
passes those exact coefficients to SciPy; it does not ask SciPy to choose a
hidden filter. The odd symmetric FIR has integer delay on the upsampled grid,
and SciPy’s centered polyphase operation compensates that delay, so the output
grid retains the input origin and reports zero residual delay. Core recomputes
the expected resampling_custody_type payload from analyzed grids and the
declared filter policy at RUN, independently of the plugin output. This custody
proves the deterministic declared design policy; it does not claim observed
sample equivalence to an independently executed kernel. Runtime/library
provenance belongs to H3 and is intentionally absent here.
Bounded waveform execution¶
H3 pairs the closed H1 candidate table with a finite resource table before any
waveform allocation. WaveformCandidateResourceTable contains one
WaveformResourceAuthority per candidate: exact WaveformGridAuthority native
and output grids including origin, exact signal/facts value types,
closed-vocabulary WaveformSupportAuthority, complex64 byte counts,
provider-specific dimensions, peak work, replay policy, and the required H2
source-evidence WaveformResamplingAuthority. Its policy is validated through
the existing H2 parameter and constant-size window-validation contracts before
execution; reduced-ratio FIR tap count and bytes are bounded analytically, so
authority construction never allocates the ratio-sized filter. Facts must be
valid EmitterFacts, and their sample rate, support interval, bandwidth, and
bandwidth semantics must agree exactly with the H3 support authority.
WaveformResourceUse is the measured execution side of that comparison.
WaveformExecutionProvider is the extension seam;
BoundedRegisteredWaveformProvider is the stock implementation over the H1
registry. StagedWaveformExecution, WaveformExecutionRequest, and
WaveformExecutionResult bind the finite table, selected candidate, typed
signal/facts, support evidence, and resource use. WaveformSourceSupportEvidence
is checked exactly against the planned support. SelectedWaveformEvidence retains the actual
selected candidate identity and support in transient execution custody; compact training labels
are published separately at dataset level. Every selector must also supply
a WaveformResourceAccounting implementation. It validates the analytic peak
and provider-specific bound from the implementation plus immutable
candidate/binding metadata before any source construction, reports execution
use, and is stored by installed identity. PLAN-dependent bindings are frozen in
the staged authority; execution constructs the source later from that exact
binding. The runtime independently checks reported native bytes against the
observable complex64 tensor.
WaveformProviderIdentity and WaveformImplementationIdentity bind installed
distribution/version, implementation, runtime, and numeric-library identities.
waveform_runtime_identity and waveform_library_identity report the executing
runtime values. WaveformRngAuthority derives selection and generation streams
from the existing Philox/SamplingKey contract. select_waveform_candidate uses
that authority without stream position; qualify_waveform_rng_contract probes
retry, insertion, call-order, fixed-seed, and two-bar replay behavior.
execute_with_global_rng_guard is the serialized qualification-only mutation
probe: it restores and refuses changes to process-global Python, NumPy, and
Torch RNG state. It is deliberately absent from the per-record hot path, which
passes only a structurally keyed local generator and remains worker-parallel.
When transmit_power consumes those corrected nominal placement facts, its
PlacementEvidencePairingRequirement proves the signal and facts have one
authority. It calibrates with the closed
total_signal_energy_over_nominal_sample_cells convention: total squared
voltage over the entire finite vector, including FIR tails outside the nominal
interval, divided by end_sample - start_sample. The node emits that convention
as typed persisted evidence and independently remeasures after scaling. This is
not an active-support claim, and zero scene padding does not dilute the result.
Legacy transmit_power without placement facts keeps its exact existing port
schema and active-support convention. The public
energy_equivalent_power_convention_type factory returns the exact closed
output type. A public
grid-changing plugin declares the exact input/output port map through
GridChangingEvidenceRequirement, whose mode is a
GridChangingEvidenceMode and whose immutable
GridEvidenceCorrectionKind plus integer factors select a closed graph-owned
correction. It implements the identifiable
GridChangingEvidenceProducer contract; graph validation follows that declaration
through select, conditional, repeat, and subgraph scopes, and RUN compares the
published facts with core’s correction. A plugin cannot replace that oracle.
Output length is computed with integer arithmetic as
(N * up + down - 1) // down. Half-open support maps by integer floor at the
left endpoint and integer ceil at the right endpoint. That interval is typed
nominal_sample_cell_interval: it describes the resampled coordinate cells,
not containment of FIR transient energy. Active-support SNR and atomic
receiver-incident contributions therefore refuse it; calculate their reference
upstream unless a later operator publishes separately validated filter-affected
support.
Evidence-custody modes use SciPy’s zero-phase, latency-compensated
resample_poly convention: sample-grid clock and origin are unchanged while
spacing changes by down / up; endpoint transients remain part of the output.
They require zero-constant padding so independently resampled contributions
retain linear superposition. Noisy inputs and stale Doppler coordinates are
refused because scalar white-noise/ENBW and Doppler coordinates need their own
corrected evidence. source_evidence proves output Nyquist containment from
the source-relative carrier offset. placed_evidence currently refuses
downsampling because ScenePlacementFacts does not carry enough information
to reconstruct that source-relative center without guessing.
The reusable typed vocabulary helpers are
support_semantics_field, resampled_emitter_facts_type, and the closed member
NOMINAL_SAMPLE_CELL_INTERVAL. recognized_support_semantics is the positive
validator: absent legacy semantics and that exact member are accepted, while
unknown present members are refused. These helpers create structural facts
variants; they do not reinterpret a legacy facts payload or mutate its original
type.
Every gridded ScenePlacementFacts has an authoritative placement_grid
TensorType leaf containing two adjacent coordinates. The leaf’s exact GridRef
is included in canonical type identity, codec bytes, and SDS metadata; the
parent Struct qualifier is only a checked edge-compatibility mirror. Ungridded
legacy scalar-box facts retain their exact old schema. placement_facts_type
constructs the structural variant, placement_grid_coordinates constructs the
leaf payload, and validate_placement_facts checks leaf coordinates and the
parent mirror. require_time_grid_ref is the common
public-source/grid-transform guard for seconds units, finite positive spacing,
finite origin, named clock, and the current one-dimensional no-Doppler source
contract.
Entity-group frames¶
Occupied-band labels and receiver noise¶
ReceiverNoiseDensityPlan with ReceiverNoiseDensityParams publishes the one
receiver-input noise-density authority. BOLTZMANN_J_PER_K and
equivalent_input_noise_density_w_per_hz expose its physical calculation.
ReceiverNoiseFromDensity/ReceiverNoiseFromDensityParams draw receiver noise
from that authority, while MemberSnrCalibration and
MemberSnrCalibrationParams calibrate member power without scaling noise.
The concept functions add_receiver_noise_from_density and
add_receiver_thermal_noise apply those two declared noise authorities.
OccupiedRfIntervals/OccupiedRfIntervalsParams publish ordered half-open
time and RF-support rows; occupied_rf_intervals_type is their typed row
schema. OccupiedBandSnr/OccupiedBandSnrParams compute analytic member SNR
from clean receiver-input power, the cited noise density, and each row’s
positive reference bandwidth.
correct_frequency_hop_event_grid maps source-grid hop intervals through the
same rational resampling phase and group-delay authority as the waveform.
Signal Atlas radar products¶
Radar processing executes the compiled nodes once without changing the
raw signed-int16 ADC authority or publishing an SDS record. adc_decode
calibrates codes once and reshapes active samples to [chirp, rx, fast_time];
RadarDecodeEvidence retains shapes, active samples, and the raw-retained but
processing-excluded tail. The owner job produces exactly 128 by 4 by 1000
complex samples and excludes its remaining 72,000 capture samples from these
auxiliary transforms.
range_transform uses a periodic Hann window and signed shifted FFT. For the
graph’s rx*conj(reference) mixer, an upchirp stationary return has negative
beat frequency, so range is -c*fbeat/(2*slope), never an absolute-value
repair. doppler_transform uses the slow-time chirp grid. Sionna-positive
Doppler denotes decreasing path length, so it maps to negative radial velocity;
positive radial velocity remains receding. range_doppler_product preserves
the receiver axis and labels voltage-squared values at the decoded-ADC plane; it neither
averages receivers nor claims impedance-normalized watts. TransformEvidence
retains window, FFT size, coherent gain, ENBW, sign, and normalization.
ca_cfar uses excluded edges and strict-greater ties. Summing M independent,
equal-scale RX powers makes the CUT Gamma(M) and N-cell reference sum Gamma(N*M),
so the threshold uses a SciPy beta-quantile ratio, not single-look exponential
alpha. Only nonnegative ranges within acquisition Nyquist and the realized IF
passband are eligible. angle_estimate consumes detections, the complex
range-Doppler cube, and exact cohort authority. M1 supports detection-keyed
azimuth on an ordered, unaliased x-axis ULA; elevation and dense angle cubes
remain deferred. Thus the visible edge is
detections + range_doppler + cohort -> angle.
target_kinematics reports solve-epoch geometric range and radial velocity.
Radar-processing truth is transient and disclaims calibrated RCS, Swerling fluctuations,
retarded scatter-event pairing, and pulsed-radar coverage. Detections and angles
remain auxiliary and never replace the raw ADC fanout.
CFAR and angle are genuine provider seams. CFARProvider and AngleProvider
declare immutable RadarProviderDescriptor contracts with RadarPortSpec
ports, strict Params, capabilities, stateless lifecycle, and thread-safety.
RegisteredRadarProviderCatalog discovers the stable
rfgen.radar_product_providers group (RADAR_PROVIDER_ENTRY_POINT_GROUP) and
resolves RadarProviderRole by explicit job override or sole default.
CoreCACFARProvider and CoreULAAngleProvider are the built-ins.
RadarProviderRegistration, RadarProviderProvenance, and
RadarProviderError make distribution/version/entry-point selection and
failures inspectable. Provider output is accepted only after
validate_detections or validate_angles checks IDs, bins, thresholds,
finiteness, grids, and exact detection linkage.
RADAR_OPERATOR_REQUIREMENTS is the independent logical-operator authority for
the required parameter schema and capabilities; offered provider capabilities
cannot redefine it.
Planning derives RadarProcessingPreflight before execution. It bounds the
simultaneously live decoded/range/range-Doppler arrays, complex64 FFT scratch,
float32 windows and CFAR plane, and dense detection/angle columns. The bound
then applies a documented 1.25 allocator/transient factor under the 256 MiB
per-record streamed-workspace profile. SciPy FFT preserves the planned
complex64 dtype; the preflight still reserves two full transform scratch arrays.
The raw ADC byte estimate covers the complete acquisition clock, including the
tail excluded from auxiliary processing; active fast time is
round(chirp_duration*acquisition_sample_rate).
The public processing values are RadarProcessingParams,
RadarDecodeEvidence, TransformEvidence, ChirpVoltageCube,
RangeSpectrum, RangeDopplerCube, and RangeDopplerPower; their public
functions are adc_decode, range_transform, doppler_transform, and
range_doppler_product. Detection and truth surfaces include CACFARParams.
DetectionTable and AngleTable retain the scalable public result as dense,
typed NumPy columns; Detection, DetectionSet, AngleRow, and AngleSet
remain compatibility/lazy-row views rather than the execution representation.
The graph names CFAR/angle inputs by meaning (range_doppler, detections,
cohort). Radar metrics likewise require the exact target_a, target_b,
clutter, leakage, other_radar, and noise ports; generic positional ports
are not accepted. The remaining parameter and truth surfaces are AngleParams,
KinematicTruthRow, ObjectInteractionTruth, NativePathTruth,
InterferenceTruth, RadarMetricsTruth, and RadarTruthBundle, with
ca_cfar, angle_estimate, and target_kinematics as their constructors.
Each nonempty target ObjectInteractionTruth row carries the cohort’s exact
geometry_id and geometry_version alongside material, state, solve epoch,
scene identity, ordered interacting objects, and conserved native slot; it does
not collapse versioned geometry authority into a generic geometry label.
The concept-owned RADAR_EMISSION_OPERATORS, RADAR_PROCESSING_OPERATORS, and
RADAR_TRUTH_OPERATORS bind canonical graph identities to NodeOperator implementations;
snr_sir remains the RF metrics calculation. DatasetPublicationUnavailableError denotes a
record-projection catalog regression; stock Signal Atlas graphs resolve and execute the dataset
publication record node instead of stopping there.
native_path_truth and object_interaction_truth perform the conserved typed
projection from native evidence and exclusive partitions.
processing_params, require_input, and validate_provider_binding close the
runtime configuration, exact declared-input, and provider/compiled-descriptor
authority checks; missing, duplicate, stale, or descriptor-drifted inputs fail.
entity_group_indexed_role decodes what a frame’s ordinals index. The role was
closed to emitter when framing was introduced, so that a contract could not
qualify a receiver or target index by coincidence. That property is about a frame
agreeing with its consumer rather than about the token being one particular word,
so the role is now a validated token that consumers must match, and a corpus whose
ordinals index targets, satellites or returns names what it indexes. emitter
remains the default, so every corpus written before this existed is unchanged.
Signal Atlas operator implementations¶
The public planning node is SampleValueOperator with typed SampleValueParams. Communications
waveform construction uses SymbolSourceOperator/SymbolSourceParams and
ModulationOperator/ModulationParams. Scene placement uses TimePlaceOperator/
TimePlaceParams and CarrierTranslateOperator/CarrierTranslateParams. The transmitter chain
uses OscillatorImpairmentOperator/OscillatorImpairmentParams, PowerAmplifierOperator/
PowerAmplifierParams, and SionnaPortBoundaryOperator/SionnaPortBoundaryParams.
Concept-owned catalogs group the remaining concrete operators without moving their behavior into
an orchestration switch: PROPAGATION_OPERATORS, RECEIVER_OPERATORS,
RADAR_EMISSION_OPERATORS, RADAR_PROCESSING_OPERATORS, and RADAR_TRUTH_OPERATORS.
Dataset record projection¶
SDS_RECORD_OPERATORS binds the canonical record/product/truth/evidence type IDs without changing
the compiled topology. Its operator consumes only declared graph inputs and projects the mandatory
raw ADC into the selected Signal Atlas source profile; auxiliary products remain a separate fanout.
receiver_chain_evidence projects the completed typed receiver-chain
authority without rerunning receiver physics.