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
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
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.observesupports"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).