Release control
Goal: control when jobs enter the shopfloor using release policies.
1) Push vs pull systems
In a push system, jobs enter the shopfloor immediately upon arrival. Simple, but can lead to high WIP and long queues.
In a pull system, jobs wait in a Pre-Shop Pool (PSP) 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 order)psp.new_job: Event that fires when a job is added
3) Composable triggers
Release policies are implemented as external SimPy processes using trigger functions from simulatte.policies.triggers:
| Trigger | Fires when | Use case |
|---|---|---|
periodic_trigger |
At regular intervals | Workload checks |
on_arrival_trigger |
New job enters PSP | Immediate decisions |
on_completion_trigger |
Job finishes at server | Starvation avoidance |
Example: compose periodic and event-driven triggers together:
from simulatte.policies.triggers import periodic_trigger, on_completion_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(triggering_job, psp):
"""Release a job when a server might starve.
The triggering_job is the job that just finished processing.
We check if its previous server is now empty.
"""
server = triggering_job.previous_server
if server is not None and server.empty and not psp.empty:
# Find a job that starts at this server
for candidate in psp.jobs:
if candidate.starts_at(server):
psp.remove(job=candidate)
psp.shopfloor.add(candidate)
break
# Register triggers
env.process(periodic_trigger(psp, 5.0, my_release_fn))
env.process(on_completion_trigger(shopfloor, psp, my_starvation_fn))
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}")
LumsCor (workload-based)
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)
SLAR (slack-based)
Event-driven release based on planned slack times. No periodic checks—releases happen when servers risk starvation or urgent jobs need insertion.
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 parameter:
allowance_factor: Slack allowance per operation (higher = more buffer time)
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 triggers can run simultaneously on the same PSP.
- The PSP's
new_jobevent is broadcast: all waiting processes receive the job. - LumsCor requires
CorrectedWIPStrategyon the shopfloor (set automatically by the builder). - SLAR is purely event-driven (no periodic trigger).