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_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 withsampler(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, |
jobs_done |
list[ProductionJob]
|
List of completed jobs in order of completion. |
metrics |
EMACollector | None
|
The default :class: |
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:
- Requests and acquires the server resource (queuing if busy)
- Executes on_before_operation hooks
- If a MaterialCoordinator is configured, waits for material delivery
- Processes the job for the specified duration
- Updates WIP via the configured WIP strategy
- Executes on_after_operation hooks
- 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:
- Waits for the inter-arrival time
- Samples a random SKU based on configured weights
- Generates a routing and processing times for the selected SKU
- Creates a ProductionJob with computed due date
- 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: |
'removed'
|
Returns:
| Type | Description |
|---|---|
ProductionJob
|
The removed job with its |
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 ofshopfloor.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.grantedandjob.released) divided by their total capacity at the end of the window.Server.worked_timeandServer.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.