Event planning

Use rfgen.planning after choosing event-start inputs and before any duration policy is available. It records causal timing provenance in receiver-capture sample coordinates; it does not choose a scheduler, infer duration, decide containment, or generate a waveform.

Quick start

from rfgen.planning import EventPlanner

unresolved = EventPlanner.compile([
    {"event_id": "e0", "anchor": "absolute", "start_sample": 0},
    {"event_id": "e1", "anchor": "predecessor", "predecessor_id": "e0"},
])
EventPlanner.write_unresolved(unresolved, scene_id="demo", root="output")

# A later duration-policy owner supplies durations; this planner does not.
resolved = EventPlanner.resolve(
    unresolved,
    {"e0": 128, "e1": 64},
    contained={"e0": True, "e1": True},
)
EventPlanner.write_resolved(resolved, scene_id="demo", root="output")

API

class EventPlanner:
    @staticmethod
    def compile(events: Iterable[EventRequest | Mapping[str, object]]) -> tuple[UnresolvedEvent, ...]: ...
    @staticmethod
    def from_time_placement(planner: BaseTimePlacement, signal: Signal, rng: torch.Generator, *, event_id_prefix: str, schedule_key: str | None = None, hop_state: Mapping[str, JsonValue] | None = None) -> tuple[UnresolvedEvent, ...]: ...
    @staticmethod
    def resolve(events: Iterable[UnresolvedEvent | Mapping[str, object]], durations: Mapping[str, int], *, contained: Mapping[str, bool]) -> tuple[ResolvedEvent, ...]: ...
    @staticmethod
    def write_unresolved(events: Iterable[UnresolvedEvent | Mapping[str, object]], *, scene_id: str, root: str | Path = ".") -> Path: ...
    @staticmethod
    def write_resolved(events: Iterable[ResolvedEvent | Mapping[str, object]], *, scene_id: str, root: str | Path = ".") -> Path: ...
    @staticmethod
    def read(path: str | Path) -> tuple[UnresolvedEvent, ...] | tuple[ResolvedEvent, ...]: ...

Record schemas

Every record uses strict integers and booleans: strings, floats, and booleans are not coerced into sample counts. hop_state is either null or a JSON object whose nested values are recursively JSON-compatible; it is retained provenance, not an instruction to generate a hop. Arrays and scalar values are rejected at the field boundary.

Field

EventRequest and unresolved artifact

Resolved artifact

event_id

Required, nonempty event identifier.

Retained unchanged.

anchor

Required: absolute or predecessor.

Retained unchanged.

start_sample

Required nonnegative receiver-capture sample for absolute; null for predecessor.

Required nonnegative concrete start.

predecessor_id

null for absolute; required nonempty ID for predecessor.

Retained unchanged.

requested_offset_samples

Nonnegative offset after a predecessor stop; defaults to 0.

Retained unchanged.

duration_policy_ref

Nullable reference to the later duration-policy owner.

Retained unchanged.

schedule_key, hop_state

Nullable causal provenance.

Retained unchanged.

duration_samples, stop_sample, contained

Forbidden.

Required: nonnegative duration, stop_sample = start_sample + duration_samples, and a boolean containment result supplied by the duration-policy owner.

EventRequest is the input model. UnresolvedEvent is the persisted form before duration resolution. ResolvedEvent is the persisted form after an external duration-policy owner supplies duration and containment results.

compile validates missing/cyclic predecessors and returns unresolved records sorted by event_id. resolve is the only place predecessor timing becomes concrete: start_sample = predecessor.stop_sample + requested_offset_samples; each supplied nonnegative duration gives stop_sample = start_sample + duration_samples. The duration-policy owner must also explicitly supply the per-event contained result, so the planner does not invent a containment policy. Resolved rows sort by (start_sample, event_id).

from_time_placement is the narrow adapter for an existing BaseTimePlacement implementation, including EventPeriodicBeacon, EventBurst, and EventFhssHop. It calls that strategy’s draw method and stores only its returned absolute starts under deterministic prefix/index IDs; it does not alter the strategy’s scheduling behavior or infer duration.

Both v1 artifacts contain schema_version, scene_id, and events. events-unresolved.json is written at artifacts/plans/<scene_id>/events-unresolved.json; it never has duration_samples, stop_sample, or contained. events-resolved.json is written beside it and requires all three fields. Both retain schedule_key and hop_state as causal provenance. The retired events.json path is rejected.

Invalid anchors, missing predecessors, and cycles raise ValidationError with event_anchor_invalid, event_predecessor_missing, or event_cycle and an event_id; duplicate IDs use event_duplicate. offset_samples is forbidden; use requested_offset_samples and handle the exact context {code: "event_field_alias_forbidden", field: "offset_samples", use: "requested_offset_samples"}. Reading the retired events.json path raises ValidationError with {code: "event_artifact_legacy_path"}. Other paths, outer-object shapes, cross-artifact rows, and noncanonical ordering are rejected rather than guessed.

Existing BaseTimePlacement, periodic-beacon, burst, and FHSS strategies may provide start inputs. This component does not reimplement their scheduling policies.

API reference