Skip to content

Events & traces API

The observable core of Simulatte: events and the bus, entities, deterministic randomness, the semantic digest, provenance, traces and KPI collectors. For a conceptual tour see the Events, traces and KPIs guide.

The stable entry points are also exported from the top-level package: Environment, Runner, Provenance, TraceRecorder, Trace, KPI, Collector, Event, DomainEvent and ObserverEvent.

Events

Typed events, state deltas and the event bus (simulatte.events). Subscribe with env.bus.subscribe(handler, types); emit with env.emit behind an env.wants guard. The Events, traces and KPIs guide explains the model.

Event dataclass

Base of all events. t and seq are stamped by :meth:Environment.emit.

DomainEvent dataclass

Bases: Event

An event that is part of the trajectory. ordinal is stamped while the projection is active.

ObserverEvent dataclass

Bases: Event

An event emitted for observers (logging, KPI samples); it never carries deltas.

event_type

event_type(
    name: str,
    *,
    version: int = 1,
    touches: Mapping[str, tuple[str, ...]] | None = None,
    presentation: frozenset[str] = frozenset(),
) -> Callable[[_E], _E]

Declare and register an event type.

The decorated class (a subclass of :class:DomainEvent or :class:ObserverEvent) becomes a frozen, slotted, keyword-only dataclass. touches maps entity kinds to the state fields the type's deltas may change; presentation names payload fields excluded from the semantic projection.

EventBus

Synchronous event delivery with nested emissions queued FIFO.

probe, when given, is called before and after each handler and returns the number of scheduled SimPy events and the number of RNG draws; a change in either means the handler scheduled a SimPy event or drew from env.rng, which raises RuntimeError (debug mode).

delivering property

delivering: bool

True while subscribers are being called.

publish

publish(event: Event) -> None

Deliver event, or queue it if delivery is in progress.

If a handler raises, the exception propagates, the queue is cleared and the bus stays usable.

subscribe

subscribe(
    handler: Handler,
    types: tuple[type[Event], ...] | Literal["*", "**"],
) -> Subscription

Deliver events to handler in subscription order.

types is a tuple of event classes (subclasses match too), "*" for every domain event, including types registered later, or "**" for every event.

wants

wants(cls: type[Event]) -> bool

Whether any subscriber listens to events of type cls.

wants_log

wants_log(priority: int) -> bool

Whether a log at priority has a subscriber (10 DEBUG, 20 INFO, etc.).

Sinks declare their minimum priority. Unfiltered subscriptions, including "**", receive every level. Component filters remain the sink's responsibility.

Subscription

A handle returned by :meth:EventBus.subscribe.

cancel

cancel() -> None

Stop delivering events to the handler (idempotent; takes effect from the next event).

Deltas dataclass

An immutable sequence of delta operations (see the module docstring for their shapes).

DeltaBuilder

Accumulates delta operations; values are frozen into wire values as they are added.

KpiSample dataclass

Bases: ObserverEvent

One sample of a KPI series; scope is the id of the owner entity.

Entities

Components that appear in events are entities with an id and a declared state schema (simulatte.entities); env.entities is the registry.

Entity

Mixin for components with an identity in their environment.

Subclasses declare kind=... in the class statement and a state_schema class attribute, and call env.entities.attach(self, ...) in their constructor once the state fields exist. Subclasses without kind inherit their parent's kind. :meth:snapshot returns the current state.

EntityRegistry

The entities of one environment: ids, live entities and lifecycle events.

Ids are unique across kinds. Live entities are held strongly in attachment order; retired ones only weakly, and their ids are never reused.

attach

attach(
    obj: Entity,
    *,
    name: str | None = None,
    label: str | None = None,
) -> str

Register obj, set its id and label and emit :class:EntityCreated; return the id.

name becomes the id; without it the id is f"{kind}-{n}". Names must be non-empty, must not contain / or NUL and must not look like a generated id of any registered kind. Raises ValueError for invalid or duplicate ids and for an object that already has an id: an entity is attached once, so a live, retired or foreign entity (attached in another environment) is rejected instead of having its id rebound. Slotted kinds may initialize the id slot to None before attaching, which spares the registry an AttributeError for every new object.

bind_node

bind_node(node: Node) -> NodeBinding

The environment-local entity of the graph node, created and attached on first use (spec §5.2).

Binding an equal Node again (two fleets sharing a graph) returns the existing binding. Raises ValueError for a different Node with the id of a bound one, and for a node id that is not a valid, unused entity name.

check_lifecycle_op

check_lifecycle_op(op: Op) -> None

Validate a create against its kind's schema and a retire against the live entities.

get

get(entity_id: str) -> Entity

The entity with entity_id: live, or retired and still referenced elsewhere (KeyError otherwise).

is_live

is_live(obj: Entity) -> bool

Whether obj is a live entity of this environment.

is_named

is_named(entity_id: str) -> bool

Whether this id was explicitly named at attachment (including retired ids).

kind_of

kind_of(entity_id: str) -> str | None

Kind of the live entity entity_id, or None.

live

live() -> tuple[Entity, ...]

Live entities in attachment order.

node_binding

node_binding(node: Node) -> NodeBinding | None

The binding of node in this environment, or None when it is not bound.

retire

retire(obj: Entity) -> None

Drop obj from the live registry and emit :class:EntityRetired.

Raises ValueError when obj is not a live entity of this environment and TypeError, leaving the registry unchanged, when its class does not support weak references (a slotted kind must list "__weakref__" in its __slots__ to be retired).

snapshot

snapshot(
    *, include_presentation: bool = True
) -> dict[str, dict[str, Wire]]

State of the live entities as wire values, sorted by id; each map has "$kind" first.

StateSchema

Bases: Mapping[str, FieldSpec]

The state fields of an entity kind, in declaration order.

Field names are non-empty strings that do not start with $ ("$kind" is reserved in state maps).

FieldSpec dataclass

Declaration of one entity state field.

wire_type is one of str, int, float, bool, array, map or any; for a collection field (collection "list" or "map") it is the type of the items. Presentation fields are excluded from the semantic projection.

Randomness

Named RNG streams and sampler binding (simulatte.rng); see Environment.rng and Environment.bind.

derive_seed

derive_seed(seed: int, name: str) -> int

Seed of stream name in an environment seeded with seed (a 128-bit integer).

SamplerDescription

Bases: Protocol[T_co]

A description of a random variate that builds samplers drawing from a given stream.

Digest and provenance

The semantic digest, the fingerprint of a run and the run manifest.

SemanticDigest

BLAKE2b-256 digest of the semantic projection of a run.

It is fed with the initial-state projection at activation and then with the projection of every domain event (see :class:DigestAccumulator). Create it with :meth:attach, before activation.

Events are encoded on a fast path that packs the precomputed canonical field order of each event class (:attr:~simulatte.events.Event.wire_semantic) and frozen values directly; it yields the bytes of :func:project_event with the rolled kind map. When an event has no presentation payload field and the projection keeps its operations unchanged, the encoded payload, deltas tail equals what a trace recorder stores for the event, and :meth:shared_tail hands it over so full recording encodes each event once.

Fingerprint dataclass

Comparable summary of a run: the semantic digest (None when none is enabled) and the KPI scalars.

kpis maps "<scope id>/<kpi name>" to the scalar, so two systems sharing an environment never merge their results (spec §12.1). kpi.sample events are observer events: they never enter the digest.

Provenance dataclass

Hashes that identify what a run was made of; each is a hash string or :data:UNAVAILABLE.

RunManifest dataclass

The manifest of a run: the requested part and, once a run finished, the final part.

complete property

complete: bool

Whether the run can be reproduced from its manifest; False until a run produced the final part.

merged

merged() -> FrozenMap

The requested and final fields in one map (the keys of the two parts are disjoint).

Traces

TraceRecorder writes a run to a trace file and Trace reads, seeks and verifies it (simulatte.trace).

TraceRecorder

Record the run of env to a trace file at path (spec §11).

Attach it before activation (it raises RuntimeError afterwards). It enables the semantic digest of env, writes the header immediately, records prelude events and, at activation, the initial state. At level="full" it records every domain event in chunks; at level="kpi" only the header, the initial state, KPI records and the footer. At both levels it records kpi.sample events (the KPI series) in KPI records. Several :meth:Environment.run calls continue the same trace.

:meth:close (also called by :meth:Environment.close) seals the open chunk, writes the remaining KPI samples, the KPI scalars and the footer, and waits until everything is on disk. clock is the time source of the latency limit and of the blocked time reported when backpressure stops the simulation.

close

close(
    outcome: Outcome | None = None,
    *,
    termination: str | None = None,
) -> None

Seal the open chunk, write the KPI scalars and the footer, and wait until all is on disk.

outcome defaults to "failed" if :meth:Environment.run raised, "cancelled" if a run was interrupted, else "completed". Raises the latched writer exception, if any; the trace then has no footer. Repeated calls do nothing.

wait_committed

wait_committed() -> None

Wait for already-enqueued records, including the activation INITIAL.

Does not seal a still-open event buffer. Managed execution uses this at its ready barrier; it is cancellable while a segment waits for credit.

ChunkLimits dataclass

When the open buffer of events is sealed into a record.

A buffer is sealed when it holds max_events events or max_bytes encoded bytes (a single larger event makes a chunk of its own), when it has been open for max_latency_s seconds of the recorder clock, or, when max_sim_window is set, before an event whose simulated time is that far from the buffer's first event. An event above max_event_bytes is recorded with a warning, or raises in debug mode.

Trace

A trace file opened for reading; create it with :meth:open.

The reader keeps the file closed between calls and reads chunks on demand, caching the last few.

catalog property

catalog: Mapping[str, CatalogEntry]

Every event type described by the trace: the header catalog plus the catalog extensions.

cursor_range property

cursor_range: tuple[Cursor, Cursor] | None

The first and last cursors :meth:state_at accepts; None if the run was never activated.

The first is the activation cursor. The last is the footer cursor of a complete full trace, else the last cursor of the last visible chunk (the activation cursor without chunks).

execution property

execution: FrozenMap | None

Optional requested/resolved/termination provenance; absent on older traces.

fingerprint property

fingerprint: Fingerprint | None

The digest and KPI scalars stored in the footer; None without a footer.

index property

index: tuple[ChunkInfo, ...]

The visible chunks, in file order.

kpi_declarations property

kpi_declarations: FrozenMap

Immutable KPI metadata keyed by scope/name; empty for older traces.

Each declaration includes its unit, kind, description and observation semantics.

kpi_series_display property

kpi_series_display: FrozenMap

Declared series display rules. Older traces return an empty map.

layout property

layout

Immutable saved layout, including opaque future versions; None in old/no-layout traces.

level property

level: str

The recording level, "full" or "kpi".

manifest property

manifest: FrozenMap

The merged manifest: the requested part and, when the footer exists, the final part.

manifest_final property

manifest_final: bool

Whether :attr:manifest includes the final part (the footer exists and holds it).

outcome property

outcome: str | None

How the run ended (completed, cancelled, failed); None without a footer.

check

check() -> None

Validate the whole container; raise :class:TraceCorrupted at the first problem.

Every record's CRC is checked and every visible chunk is decompressed and decoded within the limits; the INDEX records must agree with the footer index, and each chunk's start snapshot must equal the initial state with the deltas of all earlier chunks applied, value for value by canonical encoding (ruling R31: NaN equals NaN, -0.0 differs from 0.0). An incomplete tail is not a problem: it is reported by :attr:truncated.

events

events(
    start: Cursor | None = None, end: Cursor | None = None
) -> Iterator[TraceEvent]

The recorded domain events with start < (t, seq) <= end, in order.

These are exactly the events that lead from state_at(start) to state_at(end); a missing bound is open.

inspect_events

inspect_events() -> Iterator[TraceEvent]

Merge simulation domain and observer history, without duplicating KPI samples.

kpi_series

kpi_series() -> dict[str, list[KpiPoint]]

The KPI samples of the KPI records: for each "<scope id>/<kpi>", (cursor, value) pairs.

The cursor (t, seq) of a sample orders it among the domain events; samples are in emission order.

kpis

kpis() -> dict[str, float]

The KPI scalars of the KPI records, later records overriding earlier ones.

observer_events

observer_events(
    *, prelude: bool = False
) -> Iterator[TraceEvent]

Recorded observer events, separate from the domain replay/digest stream.

Prelude events are available only with prelude=True. Records are decoded one at a time; no observer history is retained by the reader.

open classmethod

open(
    path: str | Path, *, limits: ReaderLimits | None = None
) -> Trace

Open the trace at path.

Raises :class:TraceCorrupted for damaged or inconsistent files and files above limits, and ValueError for a format version or required feature this reader does not support.

state_at

state_at(cursor: Cursor) -> State

The replay state after every domain event at or before cursor.

Raises ValueError for a cursor outside :attr:cursor_range and :class:TraceCorrupted if the chunk it needs is damaged.

verify

verify() -> bool | Literal['not_verifiable']

Recompute the semantic digest from the initial state and the events and compare it with the footer.

Returns "not_verifiable" for kpi traces, which hold no events, and for traces without a footer digest.

ReaderLimits dataclass

Bounds enforced while reading a trace (spec §11.1); raise them for trusted local files.

max_record bounds the payload of any record, max_chunk the decompressed size of a chunk, max_depth the nesting of decoded values and max_len the length of any decoded array or map.

TraceEvent

Bases: NamedTuple

A recorded domain event: [seq, ordinal, type, t, payload, deltas] as stored in a chunk.

ChunkInfo dataclass

An entry of the chunk index: where the CHUNK record is and which events it holds.

length is the size of the record including its framing.

TraceCorrupted

Bases: Exception

The trace file is damaged beyond an incomplete tail, inconsistent, or exceeds the reader limits.

KPIs and collectors

KPI declarations, the Collector base class and observation windows (simulatte.kpi). The built-in collectors are documented under Core and Intralogistics.

KPI dataclass

Declaration of a KPI and its estimand (global C1.8).

  • name: unique within its collector; no / or NUL (results are keyed "<scope id>/<name>").
  • unit: unit of the value, for example "time" or "jobs".
  • kind: ("scalar",), ("series",) or both; a series is sampled with :meth:Collector.sample, a scalar is returned by :meth:Collector.scalars.
  • observation: the observation unit, for example "job", "operation" or "time_weighted".
  • cohort: which observations count (:meth:Collector.observe): "completed_in_window" (completed at or after the warm-up), "arrived_in_window" (arrived at or after the warm-up) or "all".
  • aggregation: how observations become the scalar; :meth:Collector.observe supports "mean", "sum", "count", "min" and "max", other values are computed by the collector.
  • clip: "window" when intervals crossing the window boundaries are clipped, else "none".
  • censoring: whether entities still in the system at the end are "exclude"-d or "include"-d.
  • ema_reset: whether an exponential moving average restarts at the warm-up.
  • empty: the scalar when there is no observation; None leaves the scalar out of the results.
  • description: free text.
  • interpolation: series display rule, "previous" (default), "linear" or "none". Exact-event inspection always uses recorded eligible samples, never a future linear bracket.
  • missing: "gap" (default) breaks at non-finite samples; "hold" uses the preceding finite value. These presentation settings do not change recorded values or scalar estimands.

Collector

Base class of KPI collectors (spec §12.1), bound to an owner entity, its scope.

Subclasses declare :attr:kpis and :attr:subscribes, implement :meth:on_event, record observations with :meth:observe and series samples with :meth:sample, and expose further results as attributes. Scalars that are not plain aggregations of observations (time-weighted means, rates) come from :meth:scalar_values. :meth:attach subscribes the collector and registers it with the environment.

State that depends on the observation window, such as :class:TimeWeighted accumulators started at the warm-up, is built in :meth:on_activate, which runs at activation, once the warm-up is fixed.

When :attr:scope_field names a payload field ("shopfloor", "fleet", "server"), events whose field holds another entity's id are not delivered; events without the field are, and the collector filters them itself.

env property

env: Environment

The environment the collector is attached to (RuntimeError before :meth:attach).

kpis class-attribute

kpis: tuple[KPI, ...] = ()

The KPIs this collector produces.

scope instance-attribute

scope = scope

The owner entity.

scope_field class-attribute

scope_field: str | None = None

Payload field naming the owner of an event, used to filter events of other scopes.

subscribes class-attribute

subscribes: (
    tuple[type[Event], ...] | Literal["*", "**"]
) = ()

The event classes delivered to :meth:on_event ("*": every domain event; "**": every event).

window property

window: Window

The observation window of the environment as of now (see :func:observation_window).

attach

attach(env: Environment) -> Self

Subscribe to :attr:subscribes, register with env and return the collector.

Raises RuntimeError when already attached, ValueError when the scope is not a live entity of env, when two KPIs share a name, or when another collector of env already produces a KPI of this scope with the same name, and TypeError for a declaration that is not a :class:KPI.

observe

observe(
    kpi: str, value: float, *, arrived: float | None = None
) -> None

Record an observation of the scalar kpi made now, if it belongs to the KPI's cohort.

completed_in_window keeps observations made at or after the warm-up, arrived_in_window those whose arrived time is at or after it, all every one. Raises ValueError when kpi is not a declared scalar, when its aggregation is not one :meth:observe supports, or when arrived is missing for an arrival cohort.

on_activate

on_activate() -> None

Build window-dependent state; the default does nothing.

Registered with :meth:Environment.on_activate by :meth:attach: it runs when the environment activates, after :meth:Environment.configure_kpis can no longer change the warm-up, or immediately when attached after activation. Like every initializer it must not schedule SimPy events. Events emitted before activation (the prelude) reach :meth:on_event before it.

on_event

on_event(event: Event) -> None

Handle a subscribed event of this scope; the default ignores it.

sample

sample(kpi: str, value: float) -> None

Emit a kpi.sample of the series kpi with value at the current time.

Built only when someone listens to :class:~simulatte.events.KpiSample. Raises ValueError when kpi is not a declared series.

scalar_values

scalar_values() -> Mapping[str, float | None]

Scalars the collector computes itself, by KPI name (None: no observation); the default has none.

They take precedence over the aggregation of :meth:observe for the same KPI.

scalars

scalars() -> dict[str, float]

The scalar KPIs keyed "<scope id>/<kpi name>", in declaration order.

A KPI without observations takes its empty value, and is left out when that is None. Raises ValueError when :meth:scalar_values names a KPI that is not a declared scalar.

Window dataclass

The observation window [start, end), or [start, end] when closed.

TimeWeighted

Time-weighted mean of a piecewise-constant signal, accumulated in constant memory.

The signal starts with value and changes at each :meth:update. Only its part from start on is accumulated, so start is the left edge of the window. The warm-up, which is that edge, is fixed only at activation (:meth:Environment.configure_kpis may be called after a collector is built), so collectors create their accumulators in :meth:Collector.on_activate with self.window.start. Updates before start only set the value carried into the window. Integrals are exactly rounded sums (D58).

value property

value: float

The current value of the signal.

mean

mean(
    window_start: float, window_end: float
) -> float | None

The mean over [window_start, window_end), the current value holding after the last update.

window_start must be the accumulator's start and window_end must not precede the last update. Returns None for an empty window. Raises ValueError for a window it cannot compute.

update

update(t: float, value: float) -> None

The signal takes value from time t on; t must not precede the previous update.

observation_window

observation_window(env: Environment) -> Window

The observation window of env as of now (global C1.8).

It starts at the warm-up. After run(until=horizon) it is [warmup, horizon), matching SimPy, which does not process events scheduled at the horizon; otherwise (exhaustion, a run in progress, no run yet) it is [warmup, now]. A run shorter than the warm-up gives the empty window [warmup, warmup).