rfgen.graph¶
rfgen.graph is the public declarative compiler. It normalizes and validates
GraphSpec, materializes deterministic structure for a sample, binds node
references, executes the resulting plan, and projects typed fields into a
SceneRecord.
The stable entry points are GraphSpec, GraphBuilder, normalize_graph,
analyze_graph_spec, materialize_graph_spec, prepare_graph,
build_scene_plan, run_scene_record, and explain_graph. Structural random draws are keyed by
rfgen.graph.randomness.StructuralPath; they do not consume generation’s
run/sample/shard seed schedule.
The compiler phases and exact boundary types are:
Static Transcript — not runnable
Audit pending; do not treat this block as a runnable example.
normalize_graph(value: GraphSpec | Mapping[str, Any]) -> GraphSpec
analyze_graph_spec(value: GraphSpec | Mapping[str, Any]) -> GraphAnalysis
materialize_graph_spec(value, *, sample_index: int, run_seed: int,
resolver: NodeResolver | None = None) -> MaterializedGraph
prepare_graph(value, resolver: NodeResolver | None = None) -> PreparedGraph
build_scene_plan(value: GraphSpec | Mapping | PreparedGraph, *,
sample_index: int, run_seed: int,
resolver: NodeResolver | None = None) -> BuiltGraph
explain_graph(value, *, view="all", sample_index=0,
run_seed=0, resolver=None) -> dict[str, Any]
Normalization parses strict authored data and canonicalizes mapping order.
Analysis resolves scopes, interfaces, typed references, bounded draws, cycles,
and record projection without running a node. Materialization requires a
nonnegative sample index and expands bounded structure into lexical node names;
its resolver, like analyze_graph_spec’s, types any port the graph leaves for
completion to infer. Pass it whenever your catalog is not the default one —
without it this phase completes against the default catalog while the rest of
your pipeline uses yours, which for a selector that shadows one core ships is a
silent disagreement rather than an error.
Plan building binds each materialized node to its catalog implementation, verifies
that authored and operator schemas match, and produces ScenePlan plus
RecordProjection. Execution runs only after these gates.
Four of the builder’s phases answer a question about the graph, not about the
sample: normalization, type completion, all-branch analysis, and the all-branch
bind. Their answers are identical for sample 0 and sample 199,999, so a caller
rendering many samples from one graph used to pay for four constants per record.
Their cost scales with the size of the authored graph text, so a configuration
with many select branches paid nearly its whole per-record budget on them.
prepare_graph runs all four once and returns a PreparedGraph; pass that to
build_scene_plan in place of the graph and the per-sample path does only
per-sample work — branch selection, ordinal resolution, $draw resolution, node
binding, plan materialization, and per-sample analysis. Passing a raw GraphSpec
or mapping still runs every phase, so the gates cannot be lost by forgetting an
argument.
A PreparedGraph is one object carrying its own completed graph, its own
all-branch evidence, and the resolver that evidence was produced against, so
there is nowhere to put a second, different graph and no way to pair evidence
with a graph it does not describe. Passing a PreparedGraph together with a
custom resolver raises AnalyzeError; prepare against that resolver instead.
Because the builder trusts a prepared graph for all four phases, PreparedGraph
refuses direct construction — prepare_graph is the only function that
stamps one, and hand-construction raises AnalyzeError. This is a stricter rule
than the validated= keyword it replaced, and deliberately so: that keyword
skipped one phase and the analysis it did not skip still rejected a bad graph,
whereas an unearned prepared graph would skip analysis too and return a plan
whose record projection dangles without raising. The guard makes forgery a
deliberate act; it does not make it impossible.
Every field of a PreparedGraph is read-only, which is what makes sharing one
across the records of a run safe: graph is a frozen model that materialization
only reads, typed is a frozen dataclass of plain rows, and the catalog behind
CatalogNodeResolver records nothing per bind.
NativeGenerationPlan prepares in its constructor and renders every record from
that one plan.prepared, which is why a generation run compiles its graph once
per plan rather than once per record. Only the authored graph crosses a process
boundary; a shard worker prepares its own against its own installed catalog.
GraphBuilder(schema_version=1) is a functional authoring helper. Chain
input, subgraph, add, output, and exactly one record, then call
build() -> GraphSpec; missing record projection raises ScenePlanError.
run_scene_record(scene_plan, projection, sample_index, run_seed, registry) -> SceneRecord validates projection, executes keyed nodes, and returns typed
fields plus compiler identity.
Corpus-axis selection schedules use the public SelectionSchedulePolicy seam.
ExactProportion preserves the fixed-phase default, while
BalancedCompleteCycles provides seed-rotated complete permutations; resolve
them with selection_schedule_policy and persist run facts with
selection_schedule_provenance.
Graph code depends on node contracts and rfgen.nodes.values. Node operators
must not import scene-plan building or execution.
Execution keeps custody checks separate by concept. gather_inputs and
validate_outputs enforce port bindings; isolate_resource_inputs,
validate_resource_inputs_unchanged, and detach_resource_consumer_outputs
protect PLAN resources. Checked labels use isolate_checked_label_inputs and
isolate_checked_label_outputs. validate_signal_transport_custody,
validate_label_custody, and validate_aggregate_custody are composed by
validate_propagation_custody. Grid-changing nodes use
expected_grid_evidence_payload and tensor_bytes_equal to compare the
declared transport evidence without re-running the transform.
Static Transcript — not runnable
Audit pending; do not treat this block as a runnable example.
from rfgen.graph import GraphSpec, normalize_graph
normalized = normalize_graph(GraphSpec.model_validate(authored_mapping))
Malformed structure raises rfgen.graph.errors.GraphError subclasses. Value
compatibility errors are defined by rfgen.nodes.values.
Signal-chain ordering¶
check_signal_chain_ordering(graph, node_classes) refuses a GraphSpec whose
realized signal paths contradict the planes its nodes declare (see
rfgen.nodes.ordering). It is called from validate_bound_graph_spec, which is
the one place that already binds every node of every branch and therefore knows
which class a selector resolved to, so the check is paid once per graph rather
than once per record. It raises AnalyzeError reporting every violation at
once, each naming both nodes and why the order is wrong.
signal_chain_edges(graph, nodes) -> tuple[tuple[str, str], ...] is the reading
half: it returns realized (producer, consumer) pairs contracted to nodes,
treating everything else on a path — a container’s output alias, or any node the
caller left out — as transparent. Only realized edges are paths; a plan edge
carries a resolved fact, not a signal. Select and conditional containers are
transparent; repeat bodies and subgraph calls are checked internally and their
declared interfaces preserve ordering across the boundary.
Propagation authority is resolved once over the completed all-branch graph as
an immutable set of exact PLAN endpoint-facts producers. Raw waveform sources
start with the empty set. A propagation producer establishes a singleton;
ApplyCIR inherits and checks one authority; a cascaded propagation effect
compares its cited endpoint producer with every nonempty upstream authority.
Every ordinary complex-voltage signal transform is transparent to the set,
including stage-less and plane-agnostic transforms, and combiners take the
union. They therefore cannot erase a mismatch merely by omitting propagation
metadata.
Container interfaces follow the same lexical graph semantics as signal-chain
ordering. Select and conditional outputs union every possible branch, subgraph
inputs and outputs resolve at the call site, and repeat collections retain
distinct local endpoint producers for distinct instances. Recursive aliases
and cycles are refused. The single-link receiver_input_boundary accepts only
one authority equal to its own endpoint producer; a mixed multi-link union is
not silently collapsed. This is producer identity, so equal link ids, equal
types, and even equal endpoint payloads from a second pose authority do not
pass.
The checked multi-link route is structurally different. An atomic
ReceiverIncidentContribution pairs each clean signal with one exact
pose-free link, emitter, and support row before collection.
ReceiverIncidentAggregate requires that exact nonempty collection origin and
unions its authorities without erasing row membership; every row must terminate
at the same receiver. Its specialized aggregate boundary accepts only signal,
facts, and custody from that same aggregate producer and retains the canonical
ordered contribution relation. Mixed receiver authority, independent
signal/link collections, or a chosen representative link fails closed.
Receiver custody is resolved by the same lexical authority walk, but as two
separate sets: checked receiver-input-boundary producers and scope-qualified
receiver-plan/index evidence. Boundary signal and facts aliases must resolve to
one checked boundary wherever a scalar noise or label consumer cites them;
noise outputs retain the exact boundary named by their declared citation.
Every boundary’s canonical posed PropagationEndpointFacts or pose-free
PropagationLinkFacts must also resolve through its receiver-identity input to
exactly one receiver PLAN authority before that authority is compared with the
boundary’s receiver measurement facts. An exact canonical payload from a
detached no-input plugin is not authority, even if its payload values would
pass the runtime comparison.
At the canonical endpoint producer itself, the compiler resolves identity and pose ownership symmetrically for transmitter and receiver through the same lexical select, conditional, subgraph, and repeat rules. Each pair must name one identical scope-qualified setup PLAN owner. The nested endpoint identity types must exactly match the bound EntityRef types, so a plugin cannot take receiver B or transmitter B as input while declaring the singleton identity vocabulary of A in its output. These checks are structural and apply to public plugins without selector or class-name privilege.
Container resolution is branch-sensitive rather than a union that can discard absence. Every select and conditional branch must independently resolve to the same singleton owner; a subgraph output must resolve to one owner; every repeat witness must be nonempty and singleton before distinct repeat-local owners are preserved as a set. An owned branch paired with an unowned but type-compatible projection therefore fails closed instead of collapsing to the owned singleton.
Static ownership cannot prove what a public PLAN implementation returns. At the
common materialization output seam, every canonical PropagationEndpointFacts
value is therefore codec-validated and compared with the four resolved input
values. Its emitted transmitter and receiver identity projections and poses
must be canonically equal to those inputs; its link reference must be internally
valid and its emitted link id must equal the declared parameter. This check is
also selector-independent, so a plugin cannot bind receiver B correctly and
then substitute receiver A’s pose during resolve().
The same materialization seam isolates every PLAN edge, not only endpoint
facts. A consumer receives a canonical codec round-trip of each input rather
than the producer’s stored Value; canonical bytes captured before resolve()
must still match afterward or materialization refuses the plugin for mutating a
bound input. Accepted outputs are codec-round-tripped again before storage, so
returning an input payload directly or retaining a return-value reference
cannot create mutable aliases into the resolved plan.
emitter_snr requires its numerator, measurement, and denominator to resolve
to the same singleton. Receiver-index evidence follows canonical facts and
ordinary complex-voltage signal transforms through all container interfaces;
combiners union their signal-input sets. A scalar claim must resolve to one
authored receiver matching the row. A repeat-local boundary or receiver plan
remains a distinct authority per instance, and recursive aliases are refused.
Authority is originated structurally only by a PLAN producer that publishes
both receiver-index-bearing facts and ReceiverEntityRef. Consequently, an
ordinary waveform plugin cannot forge or collapse custody by declaring a
parameter named receiver_index; its signal output inherits the full input
authority set.
receiver_incident_reference_snr uses the same lexical machinery for a
variable-cardinality label. Its contribution input must resolve to the exact
collection whose member producer set established the aggregate cited by both
boundary facts and custody. Its variance must cite that same checked aggregate
boundary. Select, conditional, repeat, and subgraph aliases preserve those
scope-qualified collection and boundary identities; a collection with equal
payload rows but a different producer origin is foreign. Runtime then compares
the full ordered rows against internal custody before emitting only scalar SNR
evidence, so neither static aliasing nor a forged third-party label can invent
membership.
Endpoint identity projections deliberately encode one entity in singleton enum
types, and endpoint-facts types preserve those exact nested projections. During
scene materialization ValueTypeRegistry opts only ReceiverEntityRef,
TransmitterEntityRef, PropagationEndpointFacts, and PropagationLinkFacts
into structural variants,
so multiple legitimate endpoint authorities receive exact, separate codecs.
The registry default remains one kind/one structure, and graph-edge
compatibility remains exact: opting these identity kinds into codec lookup does
not make two endpoint types interchangeable.
OrderingExemptionSpec is the escape hatch, carried on
GraphSpec.ordering_exemptions. Each entry names one producer, one
consumer, and a non-empty reason; there are no wildcards, and an exemption
naming a node the graph does not contain is refused rather than silently
protecting nothing. It sits on the graph rather than on a node’s parameters
because node parameters fold into plan_identity, and documenting an intended
ordering must not re-identify a corpus.