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.

Shared native propagation cohort

Signal Atlas binds one immutable CohortRequest containing the world, clock, solve epoch, carrier, endpoints, ports, native arrays, motion/state facts, geometry objects, ordered relationship set, seed, and solver budgets. Its closed schema checks exact referential membership before execution. A PropagationProvider has the lifecycle NEW -> OPEN -> CLOSED; closing is idempotent and a closed instance cannot execute or reopen.

The public SceneAssetProvider resolves the owner scene IDs. scene_asset_providers discovers installed providers, and resolve_scene_asset_provider validates one exact identity before use. urban_microcell, urban_canyon, and automotive_intersection to versioned, repository-owned Mitsuba XML—not to unrelated Sionna example scenes. The first two are coarse street/building layouts; the third is a coarse orthogonal road intersection. They intentionally omit facade detail, vegetation, street furniture, and traffic furniture. sedan and pedestrian similarly resolve to coarse closed bounding meshes, not high-fidelity vehicle or human models. Asset version, license, limitations, object IDs, geometry versions, and the resolved material assignment enter immutable scene authority and propagation evidence. itu_urban and itu_automotive assign every declared environment object explicitly; target materials are checked against the selected catalog. Missing, conflicting, or unsupported assignments fail before solving. The retained authority includes scene meaning, SPDX license identifier and license source, scientific exclusions, material-catalog meaning and exclusions, each proxy mesh’s meaning/version/license/exclusions, target-to-geometry and target-to-material bindings, and the post-load native-object-ID mapping. These fields deliberately identify the coarse proxies; they do not claim detailed vehicle, human, or calibrated RCS fidelity. The request does not author these descriptions. SceneAssetProvider.resolve_authority derives them from registered asset and material catalogs. The production loader recomputes that authority and requires exact model equality before loading Sionna. Target bindings, GeometryObject entries, target states, proxy versions/materials, and post-load native IDs are exact sets; missing, extra, or contradictory members fail.

SionnaRtProvider is availability-gated on the pinned Sionna RT 2.0.1 runtime. It makes exactly one PathSolver invocation for a cohort, never one invocation per relationship. The bounded fake implements the same lifecycle for contract tests and is not a production fallback. Path-affecting differences in world, materials, carrier, arrays, epoch, geometry, or solver settings require a different cohort; oscillator-only differences may reuse paths but cannot join coherently.

NativeSolveAuthority contains only inputs passed to or affecting PathSolver: provider-derived scene/material/geometry authority, endpoint/object state at the solve epoch, relationships, ports and arrays, center frequency, solver flags and budgets, and native RNG authority. Its canonical sorted JSON is the sole cache key. The bounded cache stores only immutable NativePathResult snapshots. ConversionCoherentPhaseAuthority contains capture-clock and phase-origin facts used after solving, and PathInputAuthority composes both for audit. Every cache hit reruns conversion, so evidence and each RelationshipTransfer.conversion_phase_authority belong to the requesting record. Changing phase origin reuses the native solve but cannot return stale phase authority; changing a native-solve fact creates a new solve.

sionna_paths_to_transfers is the sole native conversion boundary. It consumes absolute delays (normalize_delays=False) and consumes Sionna CIR coefficients, which already contain carrier-delay and antenna-response phase. RFGen neither reconstructs nor reapplies that phase. It preserves native Doppler/angles/interactions/object IDs/vertices and proves every returned-valid native slot and its configured antenna expansion maps exactly once. Relationship views and radar per-target/clutter/leakage partitions do not solve paths; target precedence applies to multi-object paths and the partition is exhaustive and exclusive. The other-radar relationship remains a separate full transfer.

EmittedPortWave has explicit [tx_element,time] axes and a TxPortFrame; ComplexIncidentPowerWave has [rx_element,time] axes and an RxPortFrame. They are deliberately different types because unequal transmit and receive array cardinalities are normal. apply_transfer verifies the transfer’s exact TX/RX array IDs, cardinalities, polarization basis, and frames, then applies each path independently on its TX/RX antenna axes. It uses linear complex-envelope interpolation for integer/fractional absolute delay, clips outside capture support to zero, applies the native Doppler ramp relative to the solve epoch exactly once, and sums paths coherently. This is a narrowband, frozen-path approximation; wideband delay filters and path evolution require separately named qualified operators. It never reapplies carrier phase. coherent_incident_join sums incident power waves only when grid, clock, support, carrier, oscillator, phase origin, delay/Doppler ownership, basis, ordered port frame, and impedance are identical. This join occurs before receiver noise and electronics, which are intentionally outside the shared propagation contract.

The Signal Atlas implementation catalog binds these implementations to the existing rfgen.propagation.*@1 and rfgen.receiver.coherent_incident_join@1 descriptor identities. Validation and inspection therefore retain the owner-approved topology and type IDs. Operators after the coherent join remain explicitly unavailable until their owning waves; there is no production fake-physics fallback.

The public execution surface comprises PropagationProviderCatalog, RegisteredPropagationProviderCatalog, PropagationRecordExecutor, execute_propagation_plan, and ReceiverExecutionUnavailableError. PROPAGATION_OPERATOR_IMPLEMENTATIONS, resolve_propagation_operator, shared_realization, and multi_relationship_ray_trace bind the unchanged graph descriptors. Provider contracts expose ProviderState, ProviderCapabilities, CohortExecutionCoordinator, SionnaSceneBinding, CompatibilityError, and ExecutionError; BoundedFakeProvider is test-only. Native conversion uses NativePathSlot, NativePathResult, NativePathEvidence, AttributedPath, RelationshipTransfer, and RelationshipTransferSet. Non-solving view APIs are relationship_view, RadarPathPartition, and radar_object_path_partition. Scene authority uses SceneAsset, GeometryAsset, GeometryAssetAuthority, TargetGeometryBinding, MaterialCatalog, SceneAssetProvider, and PackagedSignalAtlasAssets; load_sionna_scene materializes them and compile_scene_authority produces the immutable job authority, while native_paths_from_sionna decodes paths. SignalCompatibility is the coherent signal authority shared by both frame directions.

Stock rfgen generate binds the compiled ExecutionPlan to RecordAuthorityExecutor, revalidates ordinal zero, resolves one provider per cohort, and executes the shared solve, conversion, views/partition, application, and coherent join exactly once. It then raises the named ReceiverExecutionUnavailableError at rfgen.receiver.system_noise@1; no partial record is published. Tests may inject a bounded provider only through the public PropagationProviderCatalog. Production selects the pinned Sionna provider and fails with AvailabilityError when that optional runtime is absent.

The Sionna adapter consumes the documented 2.0.1 public tensor surface. Explicit arrays use [rx,rx_ant,tx,tx_ant,path] path and CIR axes. With Sionna synthetic arrays, native path identity, validity, delay, and raw path coefficient remain [rx,tx,path], while Paths.cir supplies the configured RX/TX antenna axes. The adapter retains the raw native coefficient as evidence, broadcasts path metadata over those antenna axes, and uses the per-element CIR coefficients for application. It rejects CIR axes that disagree with configured cardinalities, preserves depth-major interaction/object/vertex evidence, and rejects every unsupported rank or shape. Paths.cir(..., normalize_delays=False, out_type="numpy") is checked against absolute Paths.tau; for explicit arrays it also checks the documented native-coefficient/carrier-delay relationship. Application owns Doppler time evolution and never reapplies carrier phase.

NativePathEvidence carries the complete immutable PathInputAuthority: ordered relationships; endpoints, roles, ports, arrays, element coordinates and polarization basis; poses, trajectories and solve-epoch entity states; geometry and material bindings; resolved world authority; carrier; epoch; every solver flag and budget; and RNG provider/key/seed. It additionally stores native slots, raw and CIR coefficients, native-object mapping, native valid counts, expanded antenna-slot counts, truncation, provider identity/version/capabilities, and phase ownership. Its size is bounded by authored path budgets multiplied by declared array cardinalities plus the bounded cohort authority. This is typed transient shared propagation evidence; dataset publication decides SDS retention and does not implicitly place it in every training record. This layout and CIR ownership follow the official Sionna RT 2.0.1 Paths API and Paths developer guide; the dependency is pinned in pyproject.toml and uv.lock.

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 at When.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 cir and cir_facts; all eight are ordinary Transform implementations with a PLAN-staged capture_grid input, not WaveformTransform implementations. 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 epoch n to coefficient epoch n, including when l_min is 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-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:

  • awgn is synthetic target-SNR noise. It computes full-input mean-square voltage, refuses zero or power at/below minimum_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_noise computes k T B F from 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_scalingconverter_roundingconverter_clippingconverter_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_responseif_scalar_gainfir_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 single world output port at When.PLAN.

  • SceneWorldParams, its parameters: a base and a non-empty objects tuple. Also exposes structure_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 fails rfgen validate rather 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 the sampling envelope 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 the StructType a world of those parameters emits. Each object becomes one nested field named by its own object_id, which is what makes an object individually addressable and what makes wiring to an object that no longer exists fail at rfgen 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

dependency_names

dependencies a graph must bind

optional_dependency_names

dependencies a graph may bind

dependency_ports

dependency name -> port name, when they differ

dependency_whens

dependency name -> staging; default When.REALIZED

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_ports keys and dependency_whens keys are dependency names; dependency_ports values, port_when(...) and the port names in spec() 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.2 supplies constellation modulation through torchsig.signals.builders.constellation.constellation_modulator; see TorchSig documentation.

  • Sionna >=2.0.1,<2.1 supplies NR PUSCH, OFDM, FEC, Rayleigh and 3GPP channel operators, plus ray tracing. RFGen composes PUSCHTransmitter, ResourceGrid, OFDMModulator, 5G encoders, ApplyTimeChannel, and PathSolver; 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

construction, declaration, producer_provenance, spec_agreement, when_staging

nothing beyond params and wiring

always run

structural_kind

variant_params, a second parameter set differing in values only

skipped

ambient_isolation, determinism, output_conformance

every wired producer to have produced a value in the scene

skipped

evidence

the node to be a label declaring ReproClass.DETERMINISTIC

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. None means 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. None means the node has a plane but no fixed position inside it. Core exports RECEIVER_ANALOG and RECEIVER_CONVERTER as 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.

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:

state

meaning

indoor / outdoor

authored, for curriculum construction and reproducibility

omitted

read from the bound input port named by STATE_INPUT

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.