Release control and dispatching
Goal: control when jobs enter the shopfloor using release policies and apply dispatching rules for queue ordering.
1) Push vs pull systems
In a push system, jobs enter the shopfloor immediately upon arrival. Simple, but can lead to high WIP (Work-in-Progress) and long queues.
In a pull system, jobs wait in a PSP (Pre-Shop Pool) and are released only when conditions are met (e.g., workload below threshold, server starving). This controls WIP and improves flow times.
Push: Arrivals ────────────────> ShopFloor
Pull: Arrivals ───> PSP ───> ShopFloor
│
(release policy decides when)
2) The Pre-Shop Pool
The PreShopPool is a pure container with no built-in release logic. It holds jobs and provides events that external processes can monitor.
from simulatte.environment import Environment
from simulatte.psp import PreShopPool
from simulatte.shopfloor import ShopFloor
env = Environment()
shopfloor = ShopFloor(env=env)
psp = PreShopPool(env=env, shopfloor=shopfloor)
Key properties:
psp.empty: True if no jobs waitingpsp.jobs: Iterate over waiting jobs (FIFO — First-In-First-Out — order)psp.new_job: Event that fires when a job is added
3) Composing release logic
Release policies are wired to the simulation through callback registration methods on ShopFloor and PreShopPool:
| Method | Fires when | Use case |
|---|---|---|
psp.on_arrival(callback) |
New job enters PSP | Immediate decisions |
shopfloor.on_processing_end(callback) |
Job finishes at a server | Starvation avoidance |
For periodic checks, a periodic_trigger process is also available (see below).
Example: compose event-driven callbacks with a periodic trigger:
from simulatte.policies.triggers import periodic_trigger
def my_release_fn(psp):
"""Release oldest job if shopfloor WIP is low."""
if not psp.empty and len(psp.shopfloor.jobs) < 10:
job = psp.remove()
psp.shopfloor.add(job)
def my_starvation_fn(job, server):
"""Release a PSP job when a server might starve.
Called after a job finishes processing at a server.
We check if the server is now empty.
"""
if server.empty and not psp.empty:
# Find a job that starts at this server
for candidate in psp.jobs_starting_at(server):
psp.release(candidate)
break
def my_arrival_fn(job, psp):
"""Release a job immediately if its first server is idle."""
if job.servers[0].is_idle:
psp.release(job)
# Register callbacks
shopfloor.on_processing_end(my_starvation_fn)
psp.on_arrival(my_arrival_fn)
env.process(periodic_trigger(psp, 5.0, my_release_fn))
The psp.release(job) method is a convenience for psp.remove(job=job) followed by shopfloor.add(job).
Advanced: process-based triggers
For cases that require full SimPy process semantics (e.g., yielding timeouts or composing with other events), lower-level trigger functions are available in simulatte.policies.triggers:
| Trigger | Fires when |
|---|---|
periodic_trigger |
At regular intervals |
on_arrival_trigger |
New job enters PSP |
on_completion_trigger |
Job finishes at server |
These are started as SimPy processes and run in an infinite loop:
from simulatte.policies.triggers import on_arrival_trigger, on_completion_trigger
# Process-based equivalents of the callbacks above
env.process(on_arrival_trigger(psp, my_arrival_fn))
env.process(on_completion_trigger(shopfloor, psp, my_starvation_trigger_fn))
In most cases the callback APIs (psp.on_arrival, shopfloor.on_processing_end) are simpler and preferred. Use the process-based triggers when you need to yield SimPy events inside your release logic.
4) Using builders
Simulatte provides builder functions for common configurations.
Immediate release (baseline)
Jobs bypass the PSP entirely. Useful as a baseline for comparison.
from simulatte.builders import build_immediate_release_system
from simulatte.environment import Environment
env = Environment()
_, servers, shopfloor, router = build_immediate_release_system(
env,
n_servers=6,
arrival_rate=1.5,
service_rate=2.0,
)
env.run(until=1000)
print(f"Jobs completed: {len(shopfloor.jobs_done)}")
print(f"Avg time in system: {shopfloor.average_time_in_system:.2f}")
The default parameters (n_servers, arrival_rate, service_rate) reflect a standard benchmark workload — pass explicit values to model other regimes.
Optional parameters:
priority_policies: A callable(job, server) -> floatused to assign queue priorities (dispatching rules). Lower values are served first. PassNone(default) for FIFO ordering.collect_workload: IfTrue, attaches aCurrentWorkLoadCollectorthat records total remaining processing work over time (see ShopFloor extensibility).
A ready-made immediate release with SPT (Shortest Processing Time) dispatching rule is available:
from simulatte.builders import build_immediate_release_system, spt_priority_policy
_, servers, shopfloor, router = build_immediate_release_system(
env,
n_servers=6,
priority_policies=spt_priority_policy,
)
LUMS COR
Lancaster University Management School corrected order release (Thürer et al., 2012 — DOI).
Jobs are released only if adding them keeps corrected WIP at or below a workload norm. Combines periodic checks with starvation avoidance.
from simulatte.builders import build_lumscor_system
from simulatte.environment import Environment
env = Environment()
psp, servers, shopfloor, router = build_lumscor_system(
env,
check_timeout=10.0, # Check every 10 time units
wl_norm_level=5.0, # Workload threshold per server
allowance_factor=2, # Buffer for due date calculation
)
env.run(until=1000)
print(f"Jobs completed: {len(shopfloor.jobs_done)}")
print(f"Avg time in PSP: {sum(j.time_in_psp for j in shopfloor.jobs_done) / len(shopfloor.jobs_done):.2f}")
Key parameters:
check_timeout: Time between periodic release checkswl_norm_level: Maximum corrected WIP allowed per serverallowance_factor: Multiplier for due date slack (higher = more conservative)collect_workload: IfTrue, attaches aCurrentWorkLoadCollector(see ShopFloor extensibility)
Release triggers wired by the builder:
- Periodic release (
periodic_trigger): everycheck_timeouttime units, release PSP jobs (sorted by planned release date) whose corrected WIP fits within the workload norm. - Starvation release (
shopfloor.on_processing_end): when a server finishes a job:- If the server queue is empty, immediately release the PSP candidate with the earliest planned release date.
- If exactly one job remains in the queue, schedule a postponed release — the candidate is removed from PSP immediately, but enters the shopfloor after a tiny delay so the queued job acquires the server first.
- Starvation avoidance (
psp.on_arrival): when a new job enters the PSP and its first server is completely idle (empty queue and no job processing), the job is released immediately.
Queue ordering uses a PST (planned slack time) priority policy: jobs with lower PST are served first.
SLAR
Superfluous Load Avoidance Release (Land & Gaalman, 1998 — DOI).
Event-driven release based on planned slack time (PST). No periodic checks — releases are triggered by job completions at servers.
from simulatte.builders import build_slar_system
from simulatte.environment import Environment
env = Environment()
psp, servers, shopfloor, router = build_slar_system(
env,
allowance_factor=3.0, # Slack per operation
)
env.run(until=1000)
print(f"Jobs completed: {len(shopfloor.jobs_done)}")
Key parameters:
allowance_factor: Slack allowance per operation (higher = more buffer time)collect_workload: IfTrue, attaches aCurrentWorkLoadCollector(see ShopFloor extensibility)
On every job completion at a server, SLAR evaluates three branches in order:
- Empty-queue release: if the server queue is empty, immediately release the most urgent PSP candidate (lowest PST) to prevent idling.
- Urgent-job insertion: if all queued jobs are non-urgent (positive PST), release from PSP the urgent job (negative PST) with the shortest processing time, minimising disruption to the existing queue.
- Postponed starvation avoidance: if exactly one job remains in the queue, schedule a delayed release — the candidate is removed from PSP immediately (to avoid double-selection) but enters the shopfloor after a tiny delay so the queued job acquires the server first.
Additionally, a starvation avoidance callback is registered via psp.on_arrival: when a new job arrives whose first server is completely idle (empty queue and no job processing), it is released immediately.
Queue ordering uses a PST-based priority policy: jobs with lower PST are served first.
5) Comparing systems
Run all three systems and compare:
from simulatte.builders import (
build_immediate_release_system,
build_lumscor_system,
build_slar_system,
)
from simulatte.environment import Environment
from simulatte.runner import Runner
def run_system(builder_fn, builder_kwargs, until=1000):
def builder(*, env):
return builder_fn(env, **builder_kwargs)
def extract(system):
psp, servers, shopfloor, router = system
return {
"jobs_done": len(shopfloor.jobs_done),
"avg_time_in_system": shopfloor.average_time_in_system,
"avg_utilization": sum(s.utilization_rate for s in servers) / len(servers),
}
# progress=None (default) auto-enables tqdm on TTY; set False to disable
runner = Runner(builder=builder, seeds=range(5), parallel=False, extract_fn=extract)
return runner.run(until=until)
# Compare
immediate = run_system(build_immediate_release_system, {"n_servers": 6, "arrival_rate": 1.5})
lumscor = run_system(build_lumscor_system, {"check_timeout": 10, "wl_norm_level": 5, "allowance_factor": 2})
slar = run_system(build_slar_system, {"allowance_factor": 3})
for name, results in [("Immediate", immediate), ("LumsCor", lumscor), ("SLAR", slar)]:
avg_tis = sum(r["avg_time_in_system"] for r in results) / len(results)
print(f"{name}: avg time in system = {avg_tis:.2f}")
Notes
- Multiple callbacks and triggers can coexist on the same PSP and shopfloor.
psp.on_arrivalcallbacks run synchronously duringpsp.add(), before the SimPynew_jobevent fires. Process-based listeners viaon_arrival_triggerresume after.shopfloor.on_processing_endcallbacks run after the server is released (servers_exit_atis stamped andjob.previous_serveris available).- LUMS COR requires
CorrectedWIPStrategyon the shopfloor (set automatically by the builder). - SLAR is purely event-driven (no periodic trigger).