Skip to content

Core API

The core of Simulatte is a small set of cooperating objects: Environment drives the SimPy clock and carries the event bus and the log sinks; ShopFloor is the central orchestrator that tracks WIP, routes jobs, and fires hooks; ProductionJob represents a unit of work that moves through a sequence of Server resources via the Router; PreShopPool holds jobs before they are released to the floor; and Runner repeats multiple simulation replications with independent random seeds. See the architecture diagram for how these objects interact.

Core objects

Environment

Bases: Environment

Thin wrapper around simpy.Environment with an event bus and integrated logging.

Events are published with :meth:emit and delivered through :attr:bus; emitting sites guard event construction with :attr:wants. Components register themselves in :attr:entities. Randomness comes from the named streams of :meth:rng, derived from :attr:seed; components resolve their samplers with :meth:bind.

:meth:debug, :meth:info, :meth:warning and :meth:error emit log events on the bus; the log sinks of :mod:simulatte.logsinks write them out. The log_* arguments attach the default sinks: text or JSON lines to stderr or log_file, the in-memory :attr:log_history, and an SQLite database (:attr:log_db). More sinks attach with sink.attach(env); :attr:sinks lists them and :meth:close closes them.

activated property

activated: bool

Whether :meth:activate completed its initializers and captured the initial state.

collectors property

collectors: tuple[Collector, ...]

The KPI collectors attached to this environment, in attachment order.

Builders attach collectors when asked (for example collect_workload=True); find them here by type and scope.

debug_mode property

debug_mode: bool

Whether the environment validates events and subscribers (the debug constructor argument).

initial_state property

initial_state: InitialState

Canonical initial state captured at activation, after the initializers (spec §10).

A read-only view, the same object the projection listeners receive. Raises RuntimeError before activation.

layout property

layout

The attached Layout, or None (layout is imported only on explicit use).

log_db property

log_db: SQLiteSink

The SQLite sink created by log_db_path, with :meth:~simulatte.logsinks.SQLiteSink.query and :meth:~simulatte.logsinks.SQLiteSink.execute_sql.

Raises:

Type Description
RuntimeError

The environment was created without log_db_path.

log_history property

log_history: HistorySink

The in-memory history of recent log records (log_history_size of them).

Example

env.log_history.query(level="ERROR", since=100.0)

opaque_sampler_owners instance-attribute

opaque_sampler_owners: list[str] = []

Owners of the opaque samplers bound with :meth:bind, in order of first binding.

seed property

seed: int

Seed of every RNG stream of this environment.

sinks property

sinks: tuple[LogSink, ...]

The log sinks attached to this environment, in attachment order (the default ones first).

time_unit instance-attribute

time_unit = time_unit

Name of the unit of simulated time, or None.

wants instance-attribute

wants: Callable[[type[Event]], bool] = (
    self.bus._interest.__getitem__
)

wants(event_type): whether any subscriber listens to event_type (guard event construction with it).

The bound lookup of the bus's interest cache rather than a method, so that a check costs one C call and no Python frame while nobody listens (D56).

warmup property

warmup: float

The warm-up set with :meth:configure_kpis (default 0): the start of the KPI observation window.

activate

activate() -> None

End preparation and start the run (spec §10). Idempotent; :meth:run calls it on first use.

The sequence is: run the initializers in registration order, capture :attr:initial_state, notify the projection listeners and activate the projection, then execute the queued deferrable commands in call order at the current time, before any scheduled event is processed. An exception from an initializer or a command propagates; commands after a failing one are dropped. Calling it again after a failed initializer, or from an initializer, raises RuntimeError.

bind

bind(
    value: object,
    *,
    kind: Literal["scalar"],
    stream: str,
    owner: str,
) -> Callable[[], float]
bind(
    value: object,
    *,
    kind: Literal["routing"],
    stream: str,
    owner: str,
) -> Callable[[], Sequence[Any]]
bind(
    value: object,
    *,
    kind: Literal["contextual"],
    stream: str,
    owner: str,
) -> Callable[..., float]
bind(
    value: object,
    *,
    kind: BindingKind,
    stream: str,
    owner: str,
) -> Callable[..., Any]

Resolve value into the callback of a binding kind (spec §8.2).

  • scalar (() -> float): a description with sampler(rng) or a number.
  • routing (() -> Sequence[Server]): a routing description or a fixed sequence of servers.
  • contextual ((*context) -> float): a description or a number; the context is ignored.

Descriptions draw from :meth:rng (stream). Any other callable is opaque: it is returned unchanged and owner is recorded in :attr:opaque_sampler_owners. Raises ValueError for an unknown kind and TypeError for a value of no accepted form. In debug mode binding a value to a stream already bound to a different value raises ValueError: the two samplers would share one stream and the result would depend on the order of their draws.

close

close() -> None

Close the trace recorders attached to this environment, then its log sinks. Idempotent.

Every recorder and sink is closed even if one raises; the exception propagates afterwards. A closed sink receives no further event; :attr:log_history keeps its records. After closing, :meth:emit is a no-op, including events emitted by finalizers of unfinished simulation processes.

configure_kpis

configure_kpis(*, warmup: float = 0.0) -> None

Set the warm-up of the KPI observation window (spec §12.2), recorded in the requested manifest.

The window starts at warmup, a finite time >= 0; see :func:simulatte.kpi.observation_window. Raises RuntimeError once activation started and ValueError for an invalid warm-up.

debug

debug(
    message: str,
    *,
    component: str | None = None,
    **extra: Any,
) -> None

Emit a DEBUG :class:~simulatte.events.LogEvent at the current simulation time.

Parameters:

Name Type Description Default
message str

The log message

required
component str | None

Optional component name for filtering (e.g., "Server")

None
**extra Any

Additional structured data to include in the record (wire values in debug mode)

{}

emit

emit(event: Event) -> None

Stamp event and deliver it to the subscribers of its type.

Stamps t, seq and, for domain events while the projection is active, ordinal. Raises ValueError for an instance that was already emitted or a non-domain event carrying deltas, RuntimeError for a domain event emitted while subscribers are being called, and TypeError for an instance of a subclass of a registered event type that is not registered itself (it would be recorded under its parent's type with undeclared fields). Exceptions raised by subscribers propagate.

enable_digest

enable_digest() -> SemanticDigest

Attach the semantic digest of the run and return it; later calls return the same digest.

Raises RuntimeError after activation unless the digest was already enabled.

error

error(
    message: str,
    *,
    component: str | None = None,
    **extra: Any,
) -> None

Emit an ERROR :class:~simulatte.events.LogEvent; arguments as for :meth:debug.

fingerprint

fingerprint() -> Fingerprint

The digest (None unless :meth:enable_digest was called) and the KPI scalars of the run.

The scalars are those of every attached :class:~simulatte.kpi.Collector, keyed "<scope id>/<kpi name>" and sorted by key.

info

info(
    message: str,
    *,
    component: str | None = None,
    **extra: Any,
) -> None

Emit an INFO :class:~simulatte.events.LogEvent; arguments as for :meth:debug.

manifest

manifest() -> RunManifest

The manifest of the run: the requested part, plus the final part once :meth:run was called.

on_activate

on_activate(fn: Callable[[], object]) -> None

Register fn as an activation initializer.

Initializers run in registration order when the environment activates; one registered after the initializers of :meth:activate ran (for example by a projection listener, or after activation) runs immediately. While an initializer runs, scheduling any SimPy event raises RuntimeError, and simulated time must not advance.

request_projection

request_projection(
    on_initial_state: Callable[[InitialState], None],
) -> None

Request the semantic projection; on_initial_state receives the initial state at activation.

Digests and trace recorders call this when they attach; every listener receives the same read-only :attr:initial_state. Domain events get ordinals only once the projection is active, which happens after every listener received the initial state; the first domain event after activation has ordinal 0. Raises RuntimeError after activation.

rng

rng(name: str) -> random.Random

The RNG stream name, created on first use and cached.

Its seed is :func:simulatte.rng.derive_seed of :attr:seed and name, so streams are independent of each other and of their creation order. In debug mode the stream counts its draws, and drawing while subscribers are being called raises RuntimeError.

run

run(until: float | Event | None = None) -> Any

Activate the environment on first use (see :meth:activate), then run the simulation.

The stopping policy of the manifest is the one of the last call: a horizon for a numeric until, exhaustion for None. If it raises, trace recorders report the run as failed.

step

step() -> None

Process the next event in the queue.

If user interrupts the simulation via KeyboardInterrupt raise a StopSimulation exception to gently pause the simulation; trace recorders then report the run as cancelled.

volatile_metadata

volatile_metadata() -> VolatileMetadata

Host, wall-clock start and wall-clock time spent in :meth:run; separate from the manifest.

warning

warning(
    message: str,
    *,
    component: str | None = None,
    **extra: Any,
) -> None

Emit a WARNING :class:~simulatte.events.LogEvent; arguments as for :meth:debug.

ShopFloor

Bases: Entity

Central orchestrator for job flow through a manufacturing simulation.

The ShopFloor manages the complete lifecycle of production jobs as they move through a sequence of servers. It tracks work-in-progress (WIP) at each server, maintains metrics for performance monitoring, and signals events when jobs complete processing steps or finish entirely.

Extensibility is provided through composition: - on_before_operation / on_after_operation: Hooks for custom logic at each operation - wip_strategy: Pluggable WIP calculation - on_job_finished: Callbacks when jobs complete - material_coordinator: Optional material delivery coordination

Attributes:

Name Type Description
env

The simulation environment providing time and process management.

material_coordinator

Optional coordinator for material delivery.

servers list[Server]

List of servers registered with this shop floor.

jobs dict[ProductionJob, None]

Jobs currently on the shop floor, as an insertion-ordered dict keyed by job (values are None): membership, len and iteration in entry order.

jobs_done list[ProductionJob]

List of completed jobs in order of completion.

metrics EMACollector | None

The default :class:~simulatte.collectors.EMACollector (ema_* averages of completed jobs), or None when built with default_metrics=False.

wip dict[Server, float]

Dictionary mapping each server to its current WIP value.

total_time_in_system float

Cumulative time spent by all completed jobs.

job_processing_end

SimPy event triggered when any job finishes processing at a server. Recreated after each trigger.

job_finished_event

SimPy event triggered when any job completes its entire routing. Recreated after each trigger.

maximum_wip_value float

Peak total WIP observed during simulation.

maximum_shopfloor_jobs int

Peak number of concurrent jobs observed.

Example

Basic usage with hooks::

from simulatte import Environment, Server, ProductionJob, ShopFloor

def setup_hook(job, server, op_index, pt):
    yield server.env.timeout(1.0)  # 1s setup time

env = Environment()
shop_floor = ShopFloor(env=env, on_before_operation=setup_hook)
server = Server(env=env, capacity=1, shopfloor=shop_floor)

job = ProductionJob(
    env=env, sku="PART-A", servers=[server],
    processing_times=[10.0], due_date=100.0,
)

shop_floor.add(job)
env.run()

average_time_in_system property

average_time_in_system: float

Average time jobs spend in the system from first server entry to completion.

Calculated as total_time_in_system divided by the number of completed jobs. Returns 0.0 if no jobs have completed yet.

Returns:

Type Description
float

Average time in system for all completed jobs, or 0.0 if none completed.

wip_strategy property

wip_strategy: WIPStrategy

The current WIP strategy used by the shopfloor.

add

add(job: ProductionJob) -> None

Release a job from the Pre-Shop Pool onto the shop floor.

This method performs the following actions: 1. Adds the job to the active jobs 2. Updates WIP values via the configured WIP strategy 3. Emits shopfloor.entered 4. Records the PSP exit timestamp on the job 5. Spawns the main processing coroutine for the job

Parameters:

Name Type Description Default
job ProductionJob

The production job to release onto the shop floor. The job's routing must contain valid server references.

required
Note

This method modifies the job's psp_exit_at timestamp and spawns an async process. The job will begin queuing at its first server immediately after this call.

attach_dispatcher

attach_dispatcher(
    dispatcher: object, *, psp: PreShopPool | None = None
) -> None

Wire a dispatcher object's hook methods to this shopfloor.

Detects which hook methods exist on the dispatcher and registers only those that are callable. This allows partial implementations where a dispatcher only handles a subset of events.

Parameters:

Name Type Description Default
dispatcher object

Object with any combination of on_before_operation, on_after_operation, on_job_finished, on_processing_end, and on_psp_arrival methods.

required
psp PreShopPool | None

If provided and dispatcher has on_psp_arrival, registers an arrival subscription on the PSP.

None

main

main(job: ProductionJob) -> ProcessGenerator

Execute the main processing loop for a job through all its servers.

This generator manages the complete lifecycle of a job as it moves through its routing. For each server in the job's routing, it:

  1. Requests and acquires the server resource (queuing if busy)
  2. Executes on_before_operation hooks
  3. If a MaterialCoordinator is configured, waits for material delivery
  4. Processes the job for the specified duration
  5. Updates WIP via the configured WIP strategy
  6. Executes on_after_operation hooks
  7. Signals processing completion (job_processing_end event + on_processing_end callbacks)

After all operations complete, it: - Records the finish timestamp on the job - Moves the job from active (jobs) to completed (jobs_done) - Emits job.finished (the default EMACollector and other collectors update on it) - Calls on_job_finished callbacks - Signals job completion via signal_job_finished() - Retires the job from the environment's entity registry (entity.retired); jobs_done and other Python references keep it

Parameters:

Name Type Description Default
job ProductionJob

The production job to process through its routing.

required

Yields:

Type Description
ProcessGenerator

SimPy events for server requests, hooks, material delivery, and processing.

Note

This method is automatically spawned by add() and should not be called directly. It runs as a SimPy process until the job completes.

on_after_operation

on_after_operation(hook: OperationHook) -> None

Register a hook to run after each operation.

Hooks registered post-construction execute after any hooks passed via init, in registration order.

on_before_operation

on_before_operation(hook: OperationHook) -> None

Register a hook to run before each operation.

Hooks registered post-construction execute after any hooks passed via init, in registration order.

on_job_finished

on_job_finished(
    callback: Callable[[ProductionJob], None],
) -> None

Register a callback for when a job completes its entire routing.

Callbacks registered post-construction execute after any callbacks passed via init, in registration order.

on_processing_end

on_processing_end(
    callback: Callable[[ProductionJob, Server], None],
) -> None

Register a callback for when a job completes processing at any server.

Callbacks are invoked synchronously after the server is released (servers_exit_at is stamped and job.previous_server is available). This fires after each operation, not just when the job finishes its entire routing.

Note

The SimPy job_processing_end event is succeeded earlier (while the server is still held). These callbacks fire after server release and always run before SimPy process-based listeners resume.

Parameters:

Name Type Description Default
callback Callable[[ProductionJob, Server], None]

Function called with (job, server) after processing completes.

required

set_wip_strategy

set_wip_strategy(strategy: WIPStrategy) -> None

Replace the shopfloor's WIP strategy.

signal_job_finished

signal_job_finished(job: ProductionJob) -> None

Signal that a job has completed its entire routing.

This method triggers the job_finished_event with the completed job as the event value, notifying any waiting processes that a job has finished all operations. The event is then recreated for the next signal.

Unlike job_processing_end, this is only called once per job when it completes its final operation.

Parameters:

Name Type Description Default
job ProductionJob

The job that just completed its entire routing.

required
Example

Counting completed jobs::

completed = 0
while completed < target:
    job = yield shop_floor.job_finished_event
    completed += 1
    print(f"Job {job.id} completed. Total: {completed}")

snapshot

snapshot() -> dict[str, Any]

Current entity state: WIP per server id and the number of jobs in the system.

Server

Bases: PriorityResource, Entity

A server/workstation for job-shop simulation with queue and utilization tracking.

Server extends SimPy's PriorityResource to process jobs with priority-based queueing. It tracks queue lengths and utilization rates; time series of both come from :class:~simulatte.collectors.ServerTimeSeries. Its id is the name given to the constructor, or server-<n> in attachment order. When attached to a ShopFloor, the server is automatically registered on it.

Dynamic priorities: queued jobs' priorities are refreshed before every dispatch decision. sort_queue re-evaluates each queued request's job.priority_policy and rewrites req.key; _trigger_put (the SimPy hook invoked on both new-arrival and release paths) calls sort_queue before delegating to SimPy. Callers may also invoke sort_queue explicitly to observe the resulting order between events. The cost per dispatch decision is one priority_policy call per queued request; policies must be deterministic given (job, server) and the current simulation state at call time.

average_queue_length property

average_queue_length: float

Time-weighted average queue length over the simulation.

current_jobs property

current_jobs: tuple[BaseJob, ...]

Jobs currently occupying active server slots (includes hook/material phases).

empty property

empty: bool

Whether the queue is empty.

idle_time property

idle_time: float

Total time the server has been idle.

is_idle property

is_idle: bool

Whether the server has no active users and an empty queue.

queueing_jobs property

queueing_jobs: Iterable[BaseJob]

Iterator over jobs currently waiting in the queue.

utilization_rate property

utilization_rate: float

Fraction of time the server has been busy (0 to 1).

process_job

process_job(
    job: BaseJob, processing_time: float
) -> ProcessGenerator

Simulate processing a job for a given duration.

This generator yields a timeout event for the processing duration and updates worked_time, then emits server.work_credited. Should be called within a request context.

Parameters:

Name Type Description Default
job BaseJob

The job being processed.

required
processing_time float

Duration of processing in simulation time units.

required

Yields:

Type Description
ProcessGenerator

A SimPy timeout event for the processing duration.

release

release(request: ServerPriorityRequest) -> Release

Release the server after job processing.

Records the job's exit time. SimPy removes the request from users while creating the Release event; if it was there, the job's location becomes transit and job.released is emitted. Releasing a request that was never granted or was already released changes neither and emits nothing.

Parameters:

Name Type Description Default
request ServerPriorityRequest

The ServerPriorityRequest to release.

required

Returns:

Type Description
Release

A SimPy Release event.

request

request(
    *, job: BaseJob, preempt: bool = True
) -> ServerPriorityRequest

Request server access for a job with priority-based queueing.

Creates a priority request that enters the server queue. The request should be used as a context manager to ensure proper release.

Parameters:

Name Type Description Default
job BaseJob

The job requesting server access.

required
preempt bool

If True, this request can preempt lower-priority jobs.

True

Returns:

Type Description
ServerPriorityRequest

A ServerPriorityRequest to be yielded and used as a context manager.

snapshot

snapshot() -> dict[str, Any]

Current entity state: capacity, job ids of users and queue (in order) and worked time.

sort_queue

sort_queue() -> None

Refresh queued requests' priority keys and resort.

For each request in the queue, calls req.job.priority(req.server) to obtain the current priority and rewrites req.key accordingly, then sorts the queue in ascending order by the refreshed keys.

Called automatically before every dispatch decision via _trigger_put. May also be invoked explicitly by user code that has mutated priority_policy and wants to observe the new order before the next dispatch event.

Note: req.priority is not refreshed; it remains the snapshot taken at request construction. To inspect a queued job's current priority, call req.job.priority(req.server) directly.

Requires that every queued request expose job, server, time, and preempt attributes (which ServerPriorityRequest does).

When the relative order changes, emits server.queue_reordered with the minimal set of move operations.

ProductionJob

Bases: BaseJob, Entity

A production job that flows through servers with optional material requirements.

Production jobs represent manufacturing orders that require processing at one or more servers. They can optionally specify material requirements that must be delivered before processing can begin at each operation. Each job is an entity with id job-<n> (a per-environment counter).

get_materials_for_operation

get_materials_for_operation(
    op_index: int,
) -> dict[str, int]

Get material requirements for a specific operation.

Parameters:

Name Type Description Default
op_index int

The operation index (0-based).

required

Returns:

Type Description
dict[str, int]

Dictionary mapping product names to required quantities,

dict[str, int]

or empty dict if no materials required.

snapshot

snapshot() -> dict[str, Any]

Current entity state; times are floats and servers are referenced by id.

Router

Bases: Entity

Stochastic job generator that routes jobs through the simulation.

The Router continuously generates ProductionJob instances at random intervals determined by the inter-arrival distribution. Each job is assigned a randomly selected SKU, a routing through servers, and processing times sampled from configured distributions.

Jobs are routed based on system configuration: - Push system (psp=None): Jobs go directly to the ShopFloor - Pull system (psp set): Jobs queue in the PreShopPool until released

Every random draw comes from a named stream of the environment (env.rng): <id>/interarrival, <id>/sku, <id>/routing/<sku>, <id>/service/<sku>/<server id> and <id>/due/<sku>. Distribution and routing descriptions, numbers and fixed server sequences are bound to these streams with env.bind; any other callable is used unchanged and recorded as opaque in env.opaque_sampler_owners.

Upon instantiation, the Router registers itself as a SimPy process that runs for the duration of the simulation.

generate_job

generate_job() -> Generator[Timeout, None, NoReturn]

Infinite generator that creates and routes jobs at random intervals.

This method runs as a SimPy process for the simulation's duration. On each iteration it:

  1. Waits for the inter-arrival time
  2. Samples a random SKU based on configured weights
  3. Generates a routing and processing times for the selected SKU
  4. Creates a ProductionJob with computed due date
  5. Routes the job to PSP (if configured) or directly to ShopFloor

Each draw comes from its own stream, in this order per job: inter-arrival time (before the timeout), SKU, routing, one service time per operation, due-date offset.

Yields:

Type Description
Timeout

simpy.Timeout: Pauses the process until the next job arrival.

snapshot

snapshot() -> dict[str, Any]

Current entity state: the owning shop floor's id.

PreShopPool

Bases: Entity

Buffer queue for jobs awaiting shopfloor release.

A pure container with no built-in release logic. Release policies are implemented as external SimPy processes using the trigger functions from simulatte.policies.triggers.

The pool provides a new_job event that external processes can monitor to react immediately when jobs arrive (e.g., for starvation avoidance).

Example

from simulatte.policies.triggers import periodic_trigger, on_arrival_trigger psp = PreShopPool(env=env, shopfloor=shopfloor) env.process(periodic_trigger(psp, 1.0, my_release_fn)) env.process(on_arrival_trigger(psp, my_on_arrival_fn))

empty property

empty: bool

Whether the pool contains no jobs.

jobs property

jobs: Iterable[ProductionJob]

Iterate over jobs in the pool in FIFO order (oldest first).

add

add(job: ProductionJob) -> None

Add a job to the pool and signal its arrival.

Appends the job to the end of the queue and triggers the new_job event, allowing event-driven processes (e.g., starvation avoidance) to react immediately to the new arrival.

Parameters:

Name Type Description Default
job ProductionJob

The production job to add to the pool.

required

jobs_starting_at

jobs_starting_at(server: Server) -> list[ProductionJob]

Return jobs in the pool whose routing begins at the given server.

Parameters:

Name Type Description Default
server Server

The server to filter by.

required

Returns:

Type Description
list[ProductionJob]

List of jobs whose first routing server matches, in FIFO order.

on_arrival

on_arrival(
    callback: Callable[[ProductionJob, PreShopPool], None],
) -> None

Subscribe a callback to be invoked each time a job arrives in the pool.

Callbacks are invoked synchronously during add(), before the SimPy new_job event fires. No env.run() priming is needed.

Parameters:

Name Type Description Default
callback Callable[[ProductionJob, PreShopPool], None]

Function called with (job, psp) when a job arrives.

required

release

release(job: ProductionJob) -> None

Remove a job from the pool and release it to the shopfloor.

Convenience method combining remove() and shopfloor.add(). Use remove() instead if you want to discard a job without releasing it.

Parameters:

Name Type Description Default
job ProductionJob

The job to release from the pool to the shopfloor.

required

Raises:

Type Description
ValueError

If the job is not found in the pool.

remove

remove(
    *,
    job: ProductionJob | None = None,
    reason: str = "removed",
) -> ProductionJob

Remove a job from the pool and record its exit timestamp.

Supports two modes: FIFO removal (default) or specific job removal. Sets job.psp_exit_at to the current simulation time before returning and emits psp.exited.

Parameters:

Name Type Description Default
job ProductionJob | None

The specific job to remove. If None, removes the oldest job (FIFO).

None
reason str

Why the job leaves: "released", "postponed" (released after a delay) or "removed" (the default). The job's location becomes "transit" for the first two and null for a removal.

'removed'

Returns:

Type Description
ProductionJob

The removed job with its psp_exit_at timestamp updated.

Raises:

Type Description
ValueError

If a specific job is requested but not found in the pool.

snapshot

snapshot() -> dict[str, Any]

Current entity state: job ids in FIFO order and the owning shop floor's id.

Runner

Bases: Generic[S, T]

Manage repeated simulations with configurable builder and seeds.

The builder callable should accept an env: Environment parameter. Each run gets a fresh Environment(seed=seed), so its random streams depend only on its seed: sequential and parallel execution give the same results.

Supports per-simulation log files when running in parallel, enabling separate log output for each simulation instance.

run

run(until: float) -> list[T]

Run all simulations.

Parameters:

Name Type Description Default
until float

Simulation end time

required

Returns:

Type Description
list[T]

List of extracted results, one per seed

Extension points

OperationHook

Bases: Protocol

Hook called before or after each operation.

Hooks may be plain synchronous functions (returning None) or generator-based (yielding SimPy events). Both styles can coexist in the same hook list and execute in registration order.

Examples:

A synchronous logging hook::

def log_hook(job, server, op_index, processing_time):
    print(f"t={server.env.now}: {job.sku} op {op_index} on {server}")

A generator hook that adds setup time::

def setup_time_hook(job, server, op_index, processing_time):
    setup = 2.0 if job.sku.startswith("COMPLEX") else 0.5
    yield server.env.timeout(setup)

shopfloor = ShopFloor(env=env, on_before_operation=setup_time_hook)

WIPStrategy

Bases: Protocol

Strategy for calculating work-in-progress (WIP).

WIP strategies define how processing times are accumulated when jobs enter the shop floor and how they are decremented as operations complete.

Two built-in strategies are provided: - StandardWIPStrategy: Full processing time per server - CorrectedWIPStrategy: Position-discounted WIP (1/1, 1/2, 1/3, ...)

StandardWIPStrategy

Default WIP strategy: full processing time added per server.

When a job enters the shop floor, the full processing time for each operation is added to the corresponding server's WIP. When an operation completes, only that operation's processing time is decremented.

CorrectedWIPStrategy

Position-discounted WIP strategy.

Processing times are discounted by operation position: - 1st operation: full time (1/1) - 2nd operation: half time (1/2) - 3rd operation: third time (1/3) - etc.

As operations complete, remaining operations' WIP values are adjusted upward to reflect their new position in the routing.

This strategy provides a more balanced view of workload when jobs have long routings, preventing downstream servers from appearing overloaded due to jobs that haven't reached them yet.

Dispatcher

Bases: Protocol

Reference protocol showing the full dispatcher interface.

All methods are optional at runtime — attach_dispatcher wires only those that are present and callable on the dispatcher object.

This protocol is NOT runtime-checkable. It exists for documentation and IDE support. Partial implementations are explicitly supported.

Collectors

Built-in collectors on the event bus (simulatte.collectors). Each is bound to its owner (a shop floor or a server) and attached with collector.attach(env); every ShopFloor attaches an EMACollector as shopfloor.metrics unless built with default_metrics=False.

EMACollector

Bases: Collector

Exponential moving averages (EMA) of the jobs a shop floor completes, updated at each job.finished.

  • ema_makespan: time from job creation to completion.
  • ema_tardy_jobs: share of jobs finishing late, outside the due-date window.
  • ema_early_jobs: share of jobs finishing early, outside the due-date window.
  • ema_in_window_jobs: share of jobs finishing within the due-date window (±7 time units).
  • ema_time_in_psp: time spent in the pre-shop pool.
  • ema_time_in_shopfloor: time spent on the shop floor.
  • ema_total_queue_time: total time spent waiting in queues.

Each starts at 0 and moves by alpha * (value - ema) per job, so a smaller alpha weighs history more. The averages are not window KPIs: they include jobs finished during the warm-up and are not part of :meth:~simulatte.environment.Environment.fingerprint; :class:ShopFloorKPIs has the windowed results.

Example

A shop floor attaches one by default::

shop_floor = ShopFloor(env=env, ema_alpha=0.05)
env.run(until=1000)
print(shop_floor.metrics.ema_makespan)

ShopFloorTimeSeries

Bases: Collector

WIP, job count, throughput and lateness of a shop floor over time, as (time, value) lists.

  • wip_ts: total WIP (the sum of shopfloor.wip, as counted by its WIP strategy) when a job enters and after each operation.
  • job_count_ts: jobs on the shop floor when a job enters or finishes.
  • throughput_ts: completed jobs, starting with (0.0, 0), at each completion.
  • lateness_ts: lateness of each job at its completion.
Example

::

series = ShopFloorTimeSeries(shop_floor).attach(env)
env.run(until=1000)
series.plot_wip()

plot_job_count

plot_job_count() -> None

Step plot of the jobs on the shop floor over time (RuntimeError without data).

plot_lateness

plot_lateness() -> None

Scatter plot of job lateness at completion, tardy jobs red and early ones green (RuntimeError without data).

plot_throughput

plot_throughput() -> None

Step plot of the cumulative throughput over time (RuntimeError before the first completion).

plot_wip

plot_wip() -> None

Step plot of the total WIP over time (RuntimeError without data).

CurrentWorkloadCollector

Bases: Collector

Remaining processing work on a shop floor over time (wip_ts, (time, work) pairs).

The work is the sum of the processing times of the operations not yet completed by the jobs on the shop floor: queued, in progress or not yet reached. A job adds its whole routing when it enters (a point is recorded) and each operation.completed subtracts that operation's processing time (a point is recorded). Unlike :class:ShopFloorTimeSeries wip_ts, the values do not depend on the WIP strategy.

The work drops when the operation completes, not when the server is released: an after-operation hook that holds the server does not keep completed work in the total (spec §12.3). The sums are exactly rounded.

ServerTimeSeries

Bases: Collector

Queue length and utilization of one server over time.

  • qt: (time, queue length), one point per time at which a request joined or left the queue (granted or cancelled), holding the length after that time's changes. A request granted on arrival never counts as waiting.
  • ut: (time, busy slots / capacity), starting with the value when the collector is created, then one point per change (a request granted or released).

plot_qt

plot_qt() -> None

Step plot of the queue length over time (RuntimeError without data).

plot_ut

plot_ut() -> None

Step plot of the utilization over time, up to the current time.

ShopFloorKPIs

Bases: Collector

Window-aware KPIs of a shop floor (spec §12.1-§12.3), keyed "<shop floor id>/<name>".

Job KPIs are means over the jobs completed in the observation window (completion at or after the warm-up); jobs still on the shop floor at the end are excluded:

  • makespan: time from job creation to completion;
  • lateness: completion time minus due date;
  • tardiness: positive lateness, 0 for jobs on time;
  • tardy_fraction: share of jobs with positive lateness;
  • total_queue_time: total time spent waiting in queues.

throughput is the number of those jobs divided by the window length. Time-weighted KPIs are means over the window of a signal that changes with the events, clipped at the window boundaries:

  • utilization: busy slots of the shop floor's servers (job.granted and job.released) divided by their total capacity at the end of the window. Server.worked_time and Server.utilization_rate, which dispatching rules read, keep crediting work at operation completion;
  • jobs_in_system: jobs on the shop floor.

A KPI without observations, or over an empty window, is left out of :meth:scalars. Attach the collector before the run: the time-weighted signals start from the state at activation.