Duration policy validation¶
Validated with documented limitations.
1. The component¶
rfgen.planning resolves one caller-configured event duration before an event
record is created. It selects the most specific policy from class, family,
role, and scene scope; converts the chosen seconds to an integer sample count;
and records the result as an immutable planning artifact. A sample is one
time-indexed pair of in-phase and quadrature (I/Q) signal values in a receiver
capture. The component does not
schedule event starts, generate a waveform, model a transmitter or protocol,
or compose receiver IQ. Concepts / Scenes locates it
among the other planning decisions.
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: ...
Input or output |
Type and units |
Meaning |
|---|---|---|
|
|
A required scene policy plus optional class, family, and role policies. Resolution order is class, family, role, then scene. |
policy |
|
Chooses a constant duration, a bounded uniform draw, or a bounded lognormal draw. A lognormal value is |
|
uint64, an unsigned 64-bit integer |
Replays the random-policy draw exactly for an unchanged configuration. |
|
positive float; samples/s |
Converts requested seconds to an integer capture extent. |
|
positive integer; samples |
Maximum available capture extent. |
|
bool; default |
Keeps containment strict by default; |
returned |
immutable schema-version-1 record |
Stores bounds, requested seconds, realized samples, issuer, and containment result. |
from rfgen.planning import DurationPolicyResolver
plan = DurationPolicyResolver.resolve(
{"scene": {"kind": "fixed", "value_s": 0.020, "seed": 7}},
class_name=None,
family=None,
role=None,
sample_rate_hz=20_000_000,
capture_samples=2_000_000,
)
assert (plan.issuer, plan.realized_duration_samples, plan.contained) == (
"scene", 400_000, True,
)
DurationPolicyResolver.write(plan, scene_id="capture-0001", root="output")
The persisted record is
artifacts/plans/<scene_id>/durations.json. It contains exactly
schema_version, policy_kind, min_s, max_s, seed,
requested_duration_s, realized_duration_samples, issuer, contained,
and clipped_duration_samples.
2. What we validated¶
This validation establishes four load-bearing claims. Each is supported in section 3.
Hierarchical policy resolution (§3.1): the most-specific configured scope supplies the recorded duration.
Seeded bounded distributions (§3.2): fixed, uniform, and bounded-lognormal policies preserve their stated draw rules.
Sample extent and containment (§3.3): duration conversion and capture fitting use explicit, testable boundaries.
Immutable duration artifact (§3.4): the stored record preserves the resolved decision and rejects incompatible data.
Section 4 states the modeling and numerical limits.
3. Evidence per claim¶
3.1 Hierarchical policy resolution¶
The resolver checks class, family, role, then scene, returning the first
configured match and recording that scope in DurationPlan.issuer. The
acceptance test supplies four fixed policies and verifies the four selected
durations and issuers: class 0.25 s, family 0.5 s, role 0.75 s, and scene
1.0 s. At 8 samples/s, those values produce 2, 4, 6, and 8 samples,
respectively. This establishes precedence and the recorded provenance of the
duration decision.
Evidence: tests/unit/test_duration_policy.py::test_class_family_role_scene_precedence passed as part of the focused suite (15 passed). The component’s scope is intentionally limited to a caller-configured planning decision; it supplies neither a scheduler nor a physical or traffic model.
3.2 Seeded bounded distributions¶
For a fixed policy, the requested duration equals value_s. For a uniform
policy, the implementation delegates to NumPy’s Generator.uniform(min_s, max_s). For a bounded lognormal policy it draws
[ X = \exp(Y), \quad Y \sim \mathcal{N}(\mu, \sigma), ]
and accepts the first finite value satisfying min_s <= X <= max_s, with at
most 64 draws. The same configuration and seed produce equal plans. The tests
make a constant or ignored-random-generator implementation observable by
using nondegenerate bounds, and make an incorrect retry count observable with
a backend that yields no in-bound candidate.
A 256-seed diagnostic with bounds [0.010, 0.030] s observed 256 distinct,
in-bound uniform outcomes, from 0.010080565 s to 0.029952775 s, with mean
0.021017539 s. A bounded-lognormal diagnostic using mu=-3.9 and
sigma=0.24 also observed 256 distinct, in-bound values, from 0.010961572 s
to 0.029674358 s, with mean 0.020139382 s. These are checks of seeded
distribution mechanics, not measurements of RF traffic or transmitter
on-time.
Evidence: tests/unit/test_duration_policy.py::test_nondegenerate_uniform_is_seeded_and_seconds_round_half_even_to_samples and tests/unit/test_duration_policy.py::test_nondegenerate_lognormal_is_seeded_bounded_and_retries_exactly_64; the latter verifies exactly 64 rejected candidates yield {code: "duration_draw_failed", attempts: 64}. NumPy documents the uniform and lognormal generator semantics [1, 2].
3.3 Sample extent and containment¶
The resolver computes the requested capture extent as
[ n = \operatorname{round}_{\mathrm{half\ even}}(\text{seconds} \times \text{sample_rate_hz}). ]
Round-half-even sends an exact tie to the nearest even integer. Python’s
Decimal implementation therefore maps 2.5 samples to 2, not 3 [3].
At 1,000 samples/s, probes gave rejection for exactly 0.0005 s, then one
sample for 0.0005000001 s; exact 1.5, 2.5, and 3.5 sample values became
2, 2, and 4. This guards the boundary that changes whether an event
exists.
Containment is strict by default. A 20-sample request in a 15-sample capture
raises PlacementError with {code: "duration_not_contained", duration_samples: 20, capture_samples: 15}. With allow_clipping=True, the
plan records contained: false, realized_duration_samples: 15, and
clipped_duration_samples: 15. The acceptance test exercises both paths.
Evidence: tests/unit/test_duration_policy.py::test_nondegenerate_uniform_is_seeded_and_seconds_round_half_even_to_samples and tests/unit/test_duration_policy.py::test_default_no_clipping_raises_then_opt_in_records_clipped_result; Python documents the specified decimal rounding mode [3].
3.4 Immutable duration artifact¶
write creates the canonical path once using exclusive file creation, so a
second write raises FileExistsError rather than overwriting the decision.
read validates the exact version-1 outer schema and the record invariants:
requested seconds must lie within the recorded bounds, fixed policies have
equal bounds, and the clipping fields agree with containment. Tests verify a
write/read round trip, the exact field set and order, rejection of a modified
requested duration, rejection of missing or extra fields, and rejection of a
nonliteral schema version.
Evidence: tests/unit/test_duration_policy.py::test_duration_artifact_has_exact_schema_and_is_immutable, tests/unit/test_duration_policy.py::test_duration_reader_rejects_tampered_v1_semantics, and tests/unit/test_duration_policy.py::test_duration_reader_rejects_nonliteral_v1_schema, all included in the focused 15-test suite. Pydantic supplies strict, frozen model validation for the public configuration and artifact records [4].
4. Limits and what is not validated¶
Policy values and seeds are caller-supplied synthetic planning priors. This component does not establish transmitter, protocol, corpus-traffic, or transmitter-on-time realism. Such a claim requires calibration against an appropriate target population outside this component.
A duration must be strictly greater than
0.5 / sample_rate_hzto realize at least one sample. Exact half-sample ties and smaller positive durations are rejected withduration_policy_invalid; they are not rounded upward.A bounded lognormal policy needs enough probability within its stated bounds. Otherwise, 64 rejected or nonfinite candidates produce
duration_draw_failed; no endpoint is substituted.The artifact records a resolved duration and containment result only. Event count, start time, waveform, device identity, propagation, labels, receiver behavior, and IQ generation remain outside its scope.
5. References¶
NumPy
Generator.uniformdocumentation. PyPI distribution:numpy; installed version: 2.4.1. Supplies uniform draws used by the resolver and focused diagnostics.NumPy
Generator.lognormaldocumentation. PyPI distribution:numpy; installed version: 2.4.1. Supplies lognormal draws used by the resolver and focused diagnostics.Python
decimalrounding modes. Python standard library, Python 3.12. DefinesROUND_HALF_EVEN, used to convert seconds to samples.Pydantic documentation. PyPI distribution:
pydantic; installed version: 2.13.3. Supplies strict immutable input and artifact schemas.Duration policy API. Public policy, containment, artifact, and structured-error contract.