Weighted emitter selection

Use rfgen.planning when a generation job needs to draw one emitter family and one class reproducibly from an ordered mixture. This is planning context: it chooses a waveform owner, but does not construct that waveform, choose its duration, place it, apply a channel, or generate IQ. Start with Concepts / Scenes to decide how it relates to the other planning records.

What the weights mean

Family and class weights are caller-supplied synthetic sampling priors. They control the relative frequency of choices in this generation job only. They are not measurements of RF occupancy, device prevalence, protocol traffic, or prevalence in a captured corpus. Use an evidence-backed modeling step outside this API when a job needs such a claim; then pass its chosen sampling mixture to this selector.

Quick start

from rfgen.planning import WeightedEmitterSelector

plan = WeightedEmitterSelector.resolve(
    {
        "seed": 1337,
        "families": [
            {"selector": "analog", "weight": 3.0, "classes": [{"name": "am"}, {"name": "fm"}]},
            {"selector": "digital", "weight": 1.0, "classes": [{"name": "bpsk"}, {"name": "qpsk"}]},
        ],
    }
)
WeightedEmitterSelector.write(plan, scene_id="capture-0001", root="output")
print(plan.draw.selector, plan.draw.class_name)

Models and artifact

SelectionConfig takes a uint64 seed and ordered families. A family has a nonempty selector, nonnegative finite weight, and ordered classes. Each class has a nonempty name and a nonnegative finite weight; omitting a class or family weight means 1.0. YAML/list order is categorical order and is retained in the artifact. A zero-weight family or class remains recorded but is disabled for draws. Negative, nonfinite, or all-zero family/class weights raise ValidationError with code: "selection_weights_invalid".

resolve draws a family with its normalized family weights, then a class with that family’s normalized class weights, using numpy.random.Generator. The returned SelectionPlan records the raw effective weights and each class’s conditional probability; probabilities in each family sum to one within 1e-12. It writes one compact, versioned record at artifacts/plans/<scene_id>/selection.json:

{"schema_version":1,"seed":1337,"families":[{"selector":"analog","weight":3.0,"classes":[{"name":"am","weight":1.0,"probability":0.5},{"name":"fm","weight":1.0,"probability":0.5}]}],"draw":{"selector":"analog","class_name":"am"}}

The stored draw must name an enabled family and class already present in the stored ordered mixture. read validates this rather than drawing again. write creates this path once and refuses to replace an existing file, so a persisted selection remains immutable provenance for that scene ID. For the statistical check and numerical operating limit, see Weighted emitter selection validation.

Probability normalization

def normalize_probabilities(weights: tuple[float, ...]) -> tuple[float, ...]: ...

The public helper is implemented in src/rfgen/planning/selection.py. Its input container must be a nonempty tuple. Every member must be a Python int or float, but not bool, and must be finite and nonnegative; at least one member must be positive. Every container or value violation, including an empty or all-zero tuple, raises ValueError. A valid call returns a tuple of Python floats in the same order whose sum is one within the implementation’s 1e-12 absolute check.

The implementation validates every member, divides all weights by the maximum weight to avoid overflowing the sum, totals those scaled values with math.fsum, divides each scaled value by that total, and verifies the result. The private compatibility name _normalized_probabilities is the identical function object and therefore has the same behavior for valid downstream calls; new code must import the public name.

This helper is Custom/library-composed: its normalization control flow is custom and composes the Python standard library’s math.fsum. No established project dependency exposes the same scalar-tuple API, explicit validation, and overflow behavior required by this Core contract. Direct contract coverage is in tests/unit/test_sampling_public_api.py; statistical and numerical evidence is in the weighted-selection validation report.

Methods

class WeightedEmitterSelector:
    @staticmethod
    def resolve(config: SelectionConfig | dict[str, object]) -> SelectionPlan: ...
    @staticmethod
    def write(plan: SelectionPlan | dict[str, object], *, scene_id: str, root: str | Path = ".") -> Path: ...
    @staticmethod
    def read(path: str | Path) -> SelectionPlan: ...

resolve(config)

Pass a SelectionConfig or an equivalent mapping. seed is an unsigned 64-bit integer. families is an ordered nonempty list; each family supplies a nonempty selector, an optional finite nonnegative weight, and an ordered nonempty classes list. Every class has a nonempty name and optional finite nonnegative weight. Omitted weights are 1.0; zero-weight entries remain in the plan but are not selectable.

Returns an immutable SelectionPlan containing the effective raw weights, conditional class probabilities, and the selected enabled (selector, class_name) pair. Invalid configuration, negative/nonfinite weights, or an all-zero family or class distribution raises the framework’s rfgen.core.errors.ValidationError with context["code"] == "selection_weights_invalid".

write(plan, *, scene_id, root=".")

Pass a SelectionPlan or equivalent mapping, a single safe path component as scene_id, and the artifact-root directory. Returns the newly created Path root/artifacts/plans/<scene_id>/selection.json.

Invalid v1 plan content or an unsafe scene ID raises ordinary ValueError. If that exact canonical path already exists, write raises ordinary FileExistsError and leaves its bytes unchanged; it has no overwrite mode. Directory creation and filesystem-write failures propagate as ordinary OSError subclasses.

read(path)

Pass the exact canonical artifact path artifacts/plans/<scene_id>/selection.json. Returns the immutable validated SelectionPlan without drawing again. A noncanonical path, unreadable or invalid JSON payload, or a payload that violates the v1 artifact schema raises ordinary ValueError; callers do not receive a structured selection_weights_invalid error from this artifact-reading boundary.

ClassWeight, EmitterFamilyWeight, and SelectionConfig are the strict input models. SelectionClass, SelectionFamily, SelectionDraw, and SelectionPlan are immutable output/artifact models.

API reference