rfgen.scene.source_system

Use this API to describe a known set of RF-relevant devices and the directed relationships that are permitted between them before another component owns population, motion, event scheduling, propagation, or waveform generation. It gives a stable, JSON-compatible identity-and-topology record; it does not create devices or signals.

For the user-facing relationship between this environment description, stable device populations, count and event plans, emitters, channels, and scene composition, start with Concepts / Scenes.

Start here when a configuration or an external catalog already identifies the participants in one system and you need to preserve those identities without inventing a protocol or trajectory. The Scene API documents scene composition, while GeometryPose documents the typed static pose used by this record.

Build and validate a source-system record

The following constructs controller-to-aircraft links, serializes the frozen version-1 graph, and validates the serialized value again. trajectory_ref is an opaque, nonblank reference: this API records it but never resolves it.

from rfgen.core.types import GeometryPose
from rfgen.scene import SourceDevice, SourceLink, SourceSystem, namespace_hash

namespace = "example.source-system"
controller = SourceDevice(
    device_id=namespace_hash(namespace, "controller-17"),
    role="controller",
    pose=GeometryPose(
        position_m=(0.0, 0.0, 1.5),
        orientation_rad=(0.0, 0.0, 0.0),
    ),
)
aircraft = SourceDevice(
    device_id=namespace_hash(namespace, "aircraft-7"),
    role="aircraft",
    trajectory_ref="catalog:flight-7",
)
system = SourceSystem(
    system_id=namespace_hash(namespace, "training-range-a"),
    devices=(controller, aircraft),
    links=(
        SourceLink(
            link_id=namespace_hash(namespace, "control-uplink"),
            source_device=controller.device_id,
            target_device=aircraft.device_id,
            direction="uplink",
        ),
        SourceLink(
            link_id=namespace_hash(namespace, "telemetry-downlink"),
            source_device=aircraft.device_id,
            target_device=controller.device_id,
            direction="downlink",
        ),
    ),
)

payload = system.model_dump(mode="json")
restored = SourceSystem.model_validate(payload)
assert restored == system

This is a programmatic call shape for an environment with rfgen installed. model_dump(mode="json") converts tuples and the nested pose to JSON-compatible lists. model_validate rejects a modified graph that violates the rules below; it does not contact a device registry or fetch the trajectory reference.

Public API

namespace_hash

def namespace_hash(namespace: str, identifier: str) -> str: ...

Returns the canonical version-1 ID for identifier within namespace. Both string components are normalized to Unicode NFC, encoded as UTF-8, joined as normalized_namespace + "\\0" + normalized_identifier, and hashed with SHA-256. The result is a lowercase, unprefixed 64-character hexadecimal digest. For example, composed "caf\u00e9" and decomposed "cafe\u0301" components produce the same ID.

Argument

Type

Meaning

namespace

str

Logical collision boundary for an identifier.

identifier

str

Stable source identifier inside that namespace.

Raises TypeError if either argument is not a string, and ValueError if an NFC-normalized component is empty or contains a NUL character. A digest is a canonical record key, not proof of a real-world identity or origin.

namespace_hash is the provided way to generate IDs. The record models retain only the digest string, however: they accept a canonical lowercase 64-hex digest-format identifier and do not retain a namespace or identifier preimage, recompute the digest, or verify that a supplied value came from namespace_hash.

Vocabularies

DeviceRole = Literal["aircraft", "controller", "ground_station", "relay", "unknown"]
LinkDirection = Literal["uplink", "downlink", "peer"]

DeviceRole is the closed device-role vocabulary. LinkDirection describes the directed link predicate; it is not a transmitter/receiver configuration or a radio-protocol assertion.

SourceDevice

class SourceDevice(BaseModel):
    device_id: str
    role: DeviceRole
    pose: GeometryPose | None = None
    trajectory_ref: str | None = None

An immutable device identity with exactly one geometry reference.

Field

Required

Meaning

device_id

yes

Canonical lowercase 64-hex digest-format identifier. Use namespace_hash to generate one.

role

yes

One value from DeviceRole.

pose

conditional

Typed static scene-frame pose. Supply this or trajectory_ref, but not both.

trajectory_ref

conditional

Opaque nonblank string owned by a trajectory-capable component. Supply this or pose, but not both.

Pydantic rejects unknown fields, raw or malformed IDs, unsupported roles, and zero or two geometry references. It validates the ID format only; it cannot recover or verify a namespace/identifier preimage. Instances are frozen after construction.

SourceSystem

class SourceSystem(BaseModel):
    schema_version: Literal[1] = 1
    system_id: str
    devices: tuple[SourceDevice, ...]
    links: tuple[SourceLink, ...]

An immutable schema-version-1 graph. model_dump(mode="json") returns a JSON-compatible dictionary; model_validate accepts and validates the same shape. schema_version is fixed at 1, and system_id must use the same canonical lowercase 64-hex digest format as device and link IDs. Its format is validated, but its namespace/identifier provenance is not.

Each device ID and each link ID must be unique. Every link must join two distinct listed devices. An invalid whole-graph condition raises ValidationError with one of these machine-readable contexts:

Condition

context

Duplicate device ID

{"code": "source_device_invalid"}

Duplicate link ID, missing endpoint, self-link, or incompatible role/direction

{"code": "source_link_invalid", "link_id": <link ID>}