Duration policy

Use rfgen.planning when a generation job needs to resolve a standalone event duration decision. In the established manual event-plan workflow, first create unresolved events, resolve one duration for each event, then pass the resulting sample counts and containment values to EventPlanner.resolve. Start with Concepts / Scenes: duration is one planning decision alongside selected waveform class and event-start provenance. This API neither schedules starts, creates a waveform, nor composes receiver IQ.

Quick start

from rfgen.planning import DurationPolicyResolver

plan = DurationPolicyResolver.resolve(
    {
        "classes": {"qpsk": {"kind": "fixed", "value_s": 0.020, "seed": 7}},
        "scene": {"kind": "uniform", "min_s": 0.005, "max_s": 0.050, "seed": 8},
    },
    class_name="qpsk",
    family="digital",
    role="uplink",
    sample_rate_hz=20_000_000,
    capture_samples=2_000_000,
)
DurationPolicyResolver.write(plan, scene_id="capture-0001", root="output")

Configuration, policy shapes, and result

DurationPolicyConfig is a strict, frozen mapping with exactly these fields:

Field

Type and default

Meaning

classes

dict[str, DurationPolicy]; {}

Optional policy keyed by emitted class name.

families

dict[str, DurationPolicy]; {}

Optional policy keyed by emitter family.

roles

dict[str, DurationPolicy]; {}

Optional policy keyed by job-owned role name.

scene

DurationPolicy; required

Fallback policy when no more-specific key matches.

Map keys must be non-empty strings. Unknown configuration fields, coercions such as strings for numeric values, and unknown fields inside a policy are rejected. Resolution uses the first available matching policy in this order: class, family, role, scene. The returned DurationPlan.issuer records which level supplied it. Passing a non-None class, family, or role argument with an empty or non-string value is invalid.

Each DurationPolicy has a uint64 seed and one of these strict shapes:

Kind

Required fields

Draw

fixed

value_s

That positive finite duration.

uniform

min_s, max_s with min_s <= max_s

NumPy Generator.uniform(min_s, max_s).

lognormal

min_s, max_s, finite mu, positive sigma

exp(N(mu, sigma)), rejection-sampled within inclusive bounds for at most 64 attempts.

All duration values and bounds must be finite and strictly positive. A policy must contain only the fields for its declared kind: for example, fixed forbids min_s, max_s, mu, and sigma; uniform forbids value_s, mu, and sigma; and lognormal forbids value_s. Bounds are inclusive.

These values are caller-supplied synthetic planning priors. They do not establish transmitter, protocol, corpus-traffic, or transmitter-on-time realism. Calibrate the policy values against appropriate evidence before using them to make any of those claims.

Seconds become samples with decimal round-half-even arithmetic. A lognormal draw that has no in-bounds value in 64 attempts raises PlacementError with {code: "duration_draw_failed", attempts: 64}; it never silently substitutes an endpoint.

Numerical safe envelope

For a sample_rate_hz capture, the requested duration must be strictly greater than 0.5 / sample_rate_hz seconds to round to at least one sample. An exact half-sample tie rounds to zero under round-half-even, as do smaller positive durations. resolve rejects that result with its existing duration_policy_invalid error for requested_duration_s; it does not round up or create a one-sample event.

The default is strict containment. If the resolved sample count exceeds capture_samples, resolve raises PlacementError with {code: "duration_not_contained", duration_samples, capture_samples}. Pass allow_clipping=True only when the job deliberately permits truncation. The returned plan then says contained: false, records the capture-sized realized count, and records the same count as clipped_duration_samples.

DurationPlan record

resolve returns an immutable, strict version-1 DurationPlan. It is the durable record of one resolved duration decision, not an event schedule or a waveform request.

Field

Type

Invariant and meaning

schema_version

literal integer 1

Version of this artifact schema.

policy_kind

fixed, uniform, or lognormal

Kind of the selected policy.

min_s, max_s

positive finite seconds

Declared inclusive bounds; min_s <= max_s, and they are equal for fixed.

seed

uint64

Seed of the selected policy.

requested_duration_s

positive finite seconds

Draw before conversion; it lies within the recorded bounds.

realized_duration_samples

positive integer samples

Requested sample count when contained; capture-sized count when clipped.

issuer

class, family, role, or scene

Scope that supplied the policy.

contained

boolean

Whether the requested sample count fitted the capture.

clipped_duration_samples

positive integer or null

null when contained; otherwise required and equal to realized_duration_samples.

write creates the immutable artifact artifacts/plans/<scene_id>/durations.json; it refuses to replace one. Its exact schema is:

{"schema_version":1,"policy_kind":"fixed","min_s":0.02,"max_s":0.02,"seed":7,"requested_duration_s":0.02,"realized_duration_samples":400000,"issuer":"class","contained":true,"clipped_duration_samples":null}

Resolver operations

class DurationPolicyResolver:
    @staticmethod
    def resolve(config: DurationPolicyConfig | dict[str, object], *, class_name: str | None, family: str | None, role: str | None, sample_rate_hz: float, capture_samples: int, allow_clipping: bool = False) -> DurationPlan: ...
    @staticmethod
    def write(plan: DurationPlan | dict[str, object], *, scene_id: str, root: str | Path = ".") -> Path: ...
    @staticmethod
    def read(path: str | Path) -> DurationPlan: ...

resolve

resolve validates config, chooses the most-specific matching policy, draws the requested seconds deterministically from its seed, applies decimal round-half-even conversion, and checks it against the capture boundary.

Input

Required

Meaning

config

yes

A DurationPolicyConfig or mapping in the strict shape above.

class_name, family, role

yes (may each be None)

Lookup keys used in precedence order. They do not create an event.

sample_rate_hz

yes

Finite positive sample rate in samples/s.

capture_samples

yes

Positive integer receiver-capture length in samples.

allow_clipping

no; False

Whether an overlong request may become an explicit clipped plan.

It returns DurationPlan. Invalid configuration, a non-boolean allow_clipping, invalid capture_samples, or a duration that rounds to zero samples raise ValidationError with code: "duration_policy_invalid". Invalid lookup keys or sample_rate_hz raise ValueError. An overlong request with clipping disabled raises PlacementError with code: "duration_not_contained"; a bounded lognormal that cannot draw in range raises PlacementError with code: "duration_draw_failed".

write and read

write accepts a DurationPlan or matching mapping, validates it, and writes exactly one JSON object to artifacts/plans/<scene_id>/durations.json under root. scene_id must be one safe non-empty path component. The write is exclusive: an existing artifact raises FileExistsError instead of being replaced. It returns the written Path.

read accepts only that canonical durations.json path. It reads and strictly validates the exact version-1 field set and all DurationPlan invariants; it does not redraw the policy. A wrong canonical path, malformed JSON, missing or extra fields, or incompatible record raises ValueError.

Validation and limits

Duration policy validation records the tested resolution, sampling, containment, and immutable-artifact claims, plus the limits on interpreting caller-supplied synthetic priors.

API reference