Principles

Standing instructions for any agent or contributor working in rf-data-generation. This file is the single source of truth for both engineering and documentation principles. Writing-style rules (em-dashes, headings, anchors, enum-vs-string examples) live in STYLE.md.

These principles were extracted from PRs #1 through #36 of this repo, lifted to first-principle form, and cross-checked against published practices from Anthropic, Andrej Karpathy, Simon Willison, Geoffrey Litt, and Addy Osmani.

How this document is organized

This file has two parts:

  • Part 1: First principles (stable). Timeless engineering and documentation posture. These should not change as the design evolves. Per-PR examples are kept under each principle for legibility, but the principle they illustrate is what is stable.

  • Part 2: Current design contracts (mutable). The project’s concrete choices today: default backends, the current boundary ABC list, layer boundary examples, smell-test items, done-checklist scripts. These follow from Part 1 and will change as the design evolves. Treat them as the current best answer, not as principle.

When a documentation rule conflicts with a doc page, the rule wins until the doc is updated.


Part 1: First principles

These should not change as the design evolves.

Engineering principles

1. Calibrated humility

Reuse what’s verified. Treat all sources (specs, docs, your own draft) as fallible. Mark uncertainty explicitly. Pause when unsure. A merged change you cannot explain is technical debt at full interest.

Examples in this repo:

  • Default to Sionna RT/PHY for propagation, TorchSig for benchmark-compatible modulations, NumPy/SciPy/PyTorch for DSP. PR #17 deleted a custom PathLoss design that an earlier draft introduced from scratch.

  • Run a pre-submit checklist on your own draft: enum vs Literal, generic Python names vs RF terms, em-dashes, magic strings, fabricated citations, undefined acronyms (PR #16 had to define 3GPP/TDL/CDL on first use).

  • Treat the user’s docs critically. PR #23’s audit caught a 100x data-size error and three competing “next steps” headings the prose had been carrying silently.

  • Mark unverified RF claims as proposed-contract or open question rather than presenting as fact. PR #15 used adversarial verification, defaulting to “reject” on uncertainty.

  • Stop and ask before reimplementing AWGN, Doppler, IQ imbalance, or a custom shard format from scratch. The right move is search-then-surface, not charge ahead.

  • Karpathy’s framing for the same posture: keep the AI on a leash. Iron Man suit, not Iron Man drone.

2. Domain over generic

Adopt the domain’s established vocabulary rather than reusing generic CS words that collide with RF/DSP meanings. State responsibilities concretely; avoid vague architecture phrases that don’t name the method, data, or behavior involved.

Examples in this repo:

  • Sample collided with DSP “sample” and was renamed Record (PR #7).

  • FREQUENCY_TRANSLATION was wrong for what RF practice calls FREQUENCY_SHIFT (PR #14).

  • OverlapPolicy(BaseModel) and SceneOverlapPolicy(StrEnum) had clashing names; the BaseModel was renamed SceneOverlapPolicyConfig and ALLOW_P was renamed ALLOW (PR #32).

  • SignalMetadata.center_freq_hz semantically held the per-emitter offset, not an absolute carrier; renamed first to emitter_offset_hz (PR #33), then to absolute realized_carrier_hz in the from-scratch channel-pipeline redesign.

  • Sequential vs parallel composition vocabulary. Use Pipeline in names when the operation is sequential and order-dependent (non-commutative); reordering elements changes the result. Use Compose in names when the operation is parallel and order-independent (commutative); reordering inputs does not change the result. ChannelPipeline is the 14-transformation channel pipeline (TX impairments, channel propagation, RX capture, RX hardware): non-commutative, hence Pipeline. BaseSceneComposer produces a scene by summing per-emitter signals at each receiver: commutative, hence Composer. The earlier name Compose for the channel pipeline conflated these and was renamed.

  • Semantic identifiers beat positional labels. User-facing docs and APIs should surface operation names, not internal order numbers. Write “TX impairments”, “channel propagation”, “RX capture”, “RX hardware”, “DAC quantization”, and “LNA noise” instead of “Group 1”, “Group 2”, or “T10”. Keep numeric enum values only where they are the exact API contract or implementation ordering detail.

  • Before introducing a new term, check 3GPP, ITU-R, IEEE, and the major RF libraries (Sionna, TorchSig, GNU Radio).

  • Avoid “the framework owns / the layer exposes / the component manages” without naming the concrete methods or data involved.

3. Encode rules; do not describe them

Code is the contract; prose drifts. If a rule can be machine-checked, machine-check it. Anthropic’s framing: “hooks are deterministic; CLAUDE.md is advisory.”

Examples in this repo:

  • Closed sets of choices use StrEnum from rfgen.enums. Open plugin registries stay as str. YAML may use the enum string value because Pydantic deserializes it.

  • Stage ordering uses IntEnum plus a ChannelPipeline validator. PR #8 enforced monotonic chain order. PR #34 linked the Stage enum from concept pages instead of describing the order in prose.

  • One ABC hierarchy per concept. LLMClient(Protocol) was deleted in PR #23 because it competed with the inference-client ABC (now BaseInferenceClient, previously named BaseLLMClient). Concrete-to-concrete inheritance is forbidden.

  • Doc CI runs sphinx-build -W and docs_lint.py on every PR (PR #29) so style violations cannot land silently. Em-dashes, undefined LLM-voice phrasings, and unresolved cross-references all fail the build.

4. Propagate, then close

A change is not done until its consequences are reflected everywhere. Half-applied edits leave the system inconsistent; the inconsistency is silent. Match scope to reviewability. Co-locate rationale with the decision. State silent gaps explicitly.

Examples in this repo:

  • After a rename, sweep all docs, code, configs, anchors, and link maps in the same PR. PRs #30, #32, and #33 each ran tree-wide sweeps to keep the renames atomic.

  • Surface rationale on user-facing pages, not buried in background/. PR #27 moved the labeling-survey rationale onto concepts/labels.md where the decision is read.

  • One PR per logical change. Branch fresh from main. Refresh the PR title and body on every push so they cover the full diff, not just the latest commit.

  • “Looks done” is not a signal. When this PR series deferred substantive items (OverlapPolicy collision, center_freq_hz rename, PAES “contradiction”), each was named explicitly in the PR body so the deferral was not silent.

5. Context as resource

The LLM context window is finite and performance degrades as it fills. Manage it. Use subagents to extend reach without polluting the main thread.

Examples in this repo:

  • Brief subagents fully. They cannot see the main conversation. Hand over goal, context, what has been ruled out, and the form of the answer. Terse “go figure it out” prompts produce shallow work.

  • Verify subagent output. An agent’s report describes what it intended, not necessarily what it did. Spot-check the artifact.

  • Use fresh-context subagents for adversarial review (Anthropic pattern). The reviewer with no exposure to your reasoning catches biases the main thread cannot.

  • Match scope to delegation cost. A single targeted lookup is faster as a direct grep. Three or more queries on one question, parallel investigations, or wall-of-output research: spawn a subagent.

  • The PR series in this conversation spawned several subagents (27-PR mining, field-voices research, glossary em-dash sweep, center_freq_hz rename across 14 files) so the main thread stayed scannable.


Documentation principles

These rules apply to every page under docs/. They flow from the engineering principles above and govern how the docs are structured.

Use established libraries

Use established libraries, standards, and file formats when they already define the right concept. (See §1 for the underlying principle.)

Add framework code only where RF generation needs behavior those tools do not define: stable record types, IQ shape conventions, multi-emitter scene planning, backend-independent contracts, deterministic seeding, manifests, and audit records.

The current default-backend list lives in Part 2: Default backends.

Stable contracts at framework boundaries

ABCs exist for framework-owned boundaries between layers and substrates, not for every implementation detail. (See §3 for the underlying principle.)

Concrete implementations delegate specialized behavior to mature libraries. A library-backed concrete (a Sionna-backed channel, TorchSig-backed emitter, Zarr-backed writer) is preferable to custom code unless a documented contract requires otherwise.

One ABC hierarchy per concept; concrete-to-concrete inheritance is forbidden.

The current list of boundary ABCs lives in Part 2: Current boundary ABCs.

Organize by reader need

Documentation follows the standard split used by mature frameworks:

Section

Reader question

Content style

Getting Started

How do I try the system?

Short path, runnable commands, expected outputs.

Concepts

How should I think about the system?

Mental model, boundaries, data flow, tradeoffs.

How-To Guides

How do I complete a task?

Step-by-step procedure for one goal.

Reference

What exactly is the contract?

Signatures, schemas, fields, options, formats, errors.

API Reference

What are the Python interfaces?

Classes, methods, enums, lifecycle contracts.

Background

Why is it designed this way?

Rationale, alternatives, roadmap, open questions.

Do not put every kind of content on every page. A concept page can link to an API reference. An API reference can link to a how-to guide. They should not duplicate each other.

Concept page shape

A concept page explains how one part of the system fits with the rest of the system. It is not a full API reference and not a step-by-step recipe.

A good concept page usually answers:

  • What problem does this concept solve?

  • Which mature library primitive or framework object represents it?

  • What contract does the framework own?

  • What implementation details belong to external tools or plugins?

  • What are its boundaries with adjacent concepts?

  • What data flows into and out of it?

  • What choices or tradeoffs should readers understand?

  • Where should readers go for exact API signatures or task recipes?

Use this shape when it fits:

# Concept Name

## Overview

## Place In The System

## Boundaries

## Data Flow

## Minimal Example

## Design Notes

## See Also

Do not force this shape when another structure is clearer. The goal is comprehension, not template compliance.

Lead with the reader’s mental model

A page must orient the reader before it makes design conclusions. The opening paragraph names the problem the concept solves and the mental model the reader should hold; only after that do design decisions, ABC names, or framework-specific idioms appear. Terms borrowed from external ecosystems (Hydra, Pydantic, Sionna, RF/DSP, 3GPP) are introduced or linked on first use, on every page that uses them. When two passages on the same page or between adjacent pages describe overlapping ground, they must agree from the reader’s POV.

The proxy-driven /improve-docs loop catches structural and stylistic issues (em-dashes, missing anchors, stale vocabulary) but cannot see “this paragraph would lead a fresh reader to the wrong mental model.” This principle is the human-triggered counterweight: when the reader spots a confusion, it is filed in docs/.user-findings.md (human input) or appended via /reader-review (agent-simulated reader pass), and the loop prioritizes those findings over its proxy-driven work.

Examples in this repo:

  • concepts/coordinate-systems.md opens with “the framework runs without a shared scene-level RF anchor”, a design conclusion stated as the topic introduction. A reader who does not already know what an “RF anchor” is bounces. Open with the problem coordinate systems solve (multi-emitter scenes need consistent reference frames; receivers see signals in their own baseband; labels live in time-frequency rectangles that need a stable axis), then state the design decision later.

  • getting-started/first-scene.md introduces a Hydra-managed preset (heterogeneous_24ghz) with no link or one-line explanation, and overrides the same key on the same page without naming Hydra’s merge order. A reader who has not used Hydra cannot tell whether the page is contradicting itself.

  • concepts/core-types.md states the invariant “IQ and metadata always move together” and immediately defines Record with a flat iq field separated from per-emitter metadata, never naming the in-memory-vs-on-disk boundary that justifies the apparent violation. Two adjacent paragraphs contradict from the reader’s POV.

  • concepts/coordinate-systems.md describes IQ samples as being “at the absolute carrier frequency” when in fact every buffer in the framework is complex baseband and the carrier is a metadata tag. The prose lets a reader build the wrong physical model.

  • The proxy-driven loop runs that produced PRs #43, #45, and #47 cleared structural and anchor-link backlog (legitimate, useful work), but the cited reader-experience defects above were lint-clean and Sphinx-clean throughout, so the loop walked past every one of them. The fix shape is /reader-review and .user-findings.md, not more proxies.

Reference pages define exact contracts

Reference pages should be precise enough to implement against. Include the parts that matter for the contract:

  • purpose

  • constructor parameters or config fields

  • method signatures

  • parameter and return tables

  • valid values

  • required metadata

  • invariants

  • error behavior

  • examples

  • contract tests

Signatures alone are not enough. Narrative alone is not enough.

Keep boundaries precise

When two concepts have different inputs, outputs, lifecycle timing, or failure modes, describe them separately. (See §2 for the underlying principle.)

The current set of layer-precision examples lives in Part 2: Layer boundary examples.

Real usage before abstract configuration

For core APIs, show Python object usage before configuration. Then show the equivalent config that constructs the same objects.

scene = SceneComposer(
    emitters=[TorchSigEmitter(Modulation.QPSK), RadarEmitter(profile="fmcw")],
    channel=SionnaUMi(),
    labeler=JointLabeler(overlap_policy=SceneOverlapPolicy.ALLOW),
)

record = scene.generate(seed=1337)
writer.write(record)
scene:
  emitters:
    - provider: torchsig
      modulation: qpsk
    - provider: radar
      profile: fmcw
  channel:
    provider: sionna_umi
  labeler:
    overlap_policy: allow

Config is important, but it should not hide the object model.

Make claims testable

A design claim should imply a validation path. (See §1 for the underlying principle.)

Weak. Generation is deterministic.

Better. Given the same resolved config, code version, plugin versions, seed, scene ID, and asset hashes, generate(scene_id) returns byte-identical IQ and identical structured metadata across two process starts.

Corresponding test:

first  = build_pipeline(seed=1337).generate(scene_id="scene-000001")
second = build_pipeline(seed=1337).generate(scene_id="scene-000001")
assert torch.equal(first.iq, second.iq)
assert first.labels == second.labels
assert first.manifest == second.manifest

If a claim cannot be tested, mark it as rationale, hypothesis, or open question instead of presenting it as a contract.


Stop and ask when uncertain

If a smell test fires, pause and surface the question rather than charging ahead. The cost of one clarifying question is below the cost of being wrong silently.

The current smell-test items live in Part 2: Current smell tests.


What “done” looks like

A change is ready when:

  • All renames are swept across docs, code, examples, configs, schemas, and anchors.

  • Every code span in prose with an API entry links to its anchor (STYLE.md § Code-span linking rule).

  • There are no em-dashes, no LLM-voice phrasings, and no undefined acronyms.

  • Closed-set strings are StrEnum members in Python; YAML may use the string value.

  • Every new claim is testable today or marked as proposed-contract or open question.

  • The PR title and body describe the full diff, not just the latest commit.

  • Doc CI succeeds.

If you cannot meet a bar, state that explicitly in the PR body. Silent gaps are exactly what this principle exists to prevent.

The current CI scripts live in Part 2: Done-checklist concrete steps.


Part 2: Current design contracts

These are the project’s concrete choices today. They follow from Part 1 and will change as the design evolves. Treat them as the current best answer, not as principle. Each list below is current as of the most recent edit and is expected to grow or shift as the framework matures.

Default backends

The current default backends. (Principle: Use established libraries, Part 1.)

  • Sionna RT and Sionna PHY for ray-traced, statistical, and 3GPP-style propagation models.

  • TorchSig for benchmark-compatible modulation, impairment, and dataset-generation behavior.

  • NumPy, SciPy, PyTorch, and standard DSP formulations for signal math.

  • Zarr (canonical), WebDataset, HDF5, SigMF, Parquet-style metadata, and content-addressed assets for storage and interchange.

  • Hydra-style configuration composition and Pydantic-style validation for user-facing configuration.

  • Standard Python packaging, logging, testing, and CI behavior.

Current boundary ABCs

The current set of framework-owned boundary ABCs. (Principle: Stable contracts at framework boundaries, Part 1.)

Layer boundary examples

The current set of layer-precision pairs. (Principle: Keep boundaries precise, Part 1.) These will change if the layer set changes.

  • An emitter generates baseband components; a scene composer places those components in time, frequency, space, and power.

  • A channel model changes a component according to propagation and hardware effects; a receiver frontend combines components and applies receiver-specific effects.

  • A labeler derives structured labels from verified generation metadata; an annotator derives text from stored records and labels.

  • A storage writer persists canonical records; an export adapter writes training-specific shards or compatibility formats.

  • An executor runs generation work; it is not the generation pipeline itself.

Current smell tests

If any of these is true, pause and surface the question rather than charging ahead. (Principle: Stop and ask when uncertain, Part 1.) This list will grow as new failure modes surface.

  • “I am about to implement path loss, Doppler, IQ imbalance, AWGN, or a custom shard format from scratch.”

  • “I am about to define a new ABC that overlaps with an existing one.”

  • “The doc says X but I cannot find a primary source for X.”

  • “Two pages disagree about a name, version, or default; I will pick one and move on.”

  • “This concept page is becoming a 200-line dump.”

  • “I am writing Literal[...] because the enum does not exist yet.”

  • “I will add a backwards-compatibility shim because I am renaming something.”

  • “The user asked me to follow this doc as blueprint; I will assume it is precise enough.”

  • “I do not fully understand what I am about to merge, but it passes tests.”

Done-checklist concrete steps

The current build commands referenced in What “done” looks like (Part 1):

  • bash scripts/docs_build.sh succeeds (CI runs this on every PR via .github/workflows/docs-check.yml).

  • python3 scripts/docs_lint.py succeeds.

Script names and CI paths may change.


See Also

  • Style Guide for writing-style rules.

  • The project README and CLAUDE.md at the repo root cover the headline summary and the agent-facing operational summary.