Comparing Release Policies
This tutorial builds a six-server job shop and compares three release policies side by side:
- Immediate Release — push system, no workload control
- LumsCor — load-based pull system (Land's LUMS-COR)
- SLAR — superfluous-load avoidance pull system
By running each policy with the same random seed you get a controlled, apples-to-apples comparison.
Prefer to run it live? The workload-control and WIP-cap galleries compare these and related policies in-browser.
Why release control matters
In a push system every arriving job enters the shop floor immediately. At high utilisation (≈ 90 %) queues grow without bound and most jobs finish late. A release policy holds new jobs in a pre-shop pool (PSP) and only releases them when servers can absorb the load — trading a little PSP waiting time for dramatically shorter queue times and better due-date performance.
Simulatte ships three of the ready-made builder functions that encapsulate the full wiring of shop floor, servers, pre-shop pool, router, and release triggers. That makes policy comparison a matter of calling each builder in turn.
Scenario setup
Six servers, exponential inter-arrivals (λ ≈ 1.56/time-unit), truncated 2-Erlang service times (µ = 2.0), uniform due dates at 30–45 time units after arrival. Utilisation ≈ 88 %. Simulation duration: 2 000 time units. Fixed seed: 42.
Running the three policies
Each builder returns a BuiltSystem named tuple — (psp, servers, shopfloor, router, policy). The PSP is None for the push system and a PreShopPool instance for the pull systems; policy is the wired release policy (or None for the push baseline).
"""Compare Immediate Release, LumsCor, and SLAR release policies.
Runs each policy for a fixed simulation time using the same random seed,
then prints a comparison table of key performance metrics.
Simplification note: uses a single run per policy (same seed for each)
rather than multi-seed averaging, to keep the script self-contained and
deterministic. The same seed gives identical arrival / service-time
streams to all three policies (common-random-numbers design).
"""
from __future__ import annotations
from simulatte.builders import (
build_immediate_release_system,
build_lumscor_system,
build_slar_system,
)
from simulatte.environment import Environment
SEED = 42
SIM_TIME = 2000.0
def run_policy(builder_fn, seed: int = SEED, until: float = SIM_TIME) -> dict:
"""Run a single simulation with the given builder and seed."""
with Environment(seed=seed) as env:
psp, servers, shopfloor, _router, _policy = builder_fn(env)
env.run(until=until)
done = shopfloor.jobs_done
n_done = len(done)
psp_size = len(psp) if psp is not None else 0
n_late = sum(1 for j in done if j.late)
mean_tardiness = sum(max(0.0, j.lateness) for j in done) / n_done if n_done else 0.0
mean_makespan = sum(j.makespan for j in done) / n_done if n_done else 0.0
avg_util = sum(s.utilization_rate for s in servers) / len(servers)
# End-of-simulation WIP: remaining work queued / in-progress on the shop floor
end_wip = sum(shopfloor.wip.values())
return {
"completed": n_done,
"psp_remaining": psp_size,
"late_pct": n_late / n_done * 100 if n_done else 0.0,
"mean_tardiness": mean_tardiness,
"mean_makespan": mean_makespan,
"end_wip": end_wip,
"avg_util_pct": avg_util * 100,
}
def main() -> None:
policies = {
"Immediate": lambda env: build_immediate_release_system(env=env),
"LumsCor": lambda env: build_lumscor_system(env=env, check_timeout=10.0, wl_norm_level=5.0, allowance_factor=2),
"SLAR": lambda env: build_slar_system(env=env, allowance_factor=3.0),
}
results = {name: run_policy(fn) for name, fn in policies.items()}
print(f"Release policy comparison (seed={SEED}, sim_time={SIM_TIME:.0f})")
print()
headers = ["Policy", "Done", "PSP left", "Late %", "Mean tardy", "Mean span", "End WIP", "Util %"]
widths = [10, 6, 8, 7, 10, 10, 9, 7]
header_line = " ".join(h.ljust(w) for h, w in zip(headers, widths, strict=False))
print(header_line)
print("-" * len(header_line))
for name, r in results.items():
row = [
name.ljust(widths[0]),
str(r["completed"]).ljust(widths[1]),
str(r["psp_remaining"]).ljust(widths[2]),
f"{r['late_pct']:.1f}%".ljust(widths[3]),
f"{r['mean_tardiness']:.2f}".ljust(widths[4]),
f"{r['mean_makespan']:.2f}".ljust(widths[5]),
f"{r['end_wip']:.1f}".ljust(widths[6]),
f"{r['avg_util_pct']:.1f}%".ljust(widths[7]),
]
print(" ".join(row))
print()
print("Columns:")
print(" Done = jobs completed by sim_time")
print(" PSP left = jobs still held in the pre-shop pool at end")
print(" Late % = % of completed jobs that finished after their due date")
print(" Mean tardy = average tardiness (0 for on-time / early jobs)")
print(" Mean span = average makespan (creation -> finish)")
print(" End WIP = total remaining work on the shop floor at sim_time")
print(" Util % = average server utilization")
if __name__ == "__main__":
main()
The complete runnable script is at examples/comparing_release_policies.py.
Run it:
uv run python examples/comparing_release_policies.py
Results
Release policy comparison (seed=42, sim_time=2000)
Policy Done PSP left Late % Mean tardy Mean span End WIP Util %
---------------------------------------------------------------------------------
Immediate 2963 0 10.5% 1.10 19.40 243.2 87.6%
LumsCor 2969 49 8.9% 0.97 18.77 52.7 87.4%
SLAR 2974 49 2.5% 0.20 18.53 49.7 87.6%
Columns:
Done = jobs completed by sim_time
PSP left = jobs still held in the pre-shop pool at end
Late % = % of completed jobs that finished after their due date
Mean tardy = average tardiness (0 for on-time / early jobs)
Mean span = average makespan (creation -> finish)
End WIP = total remaining work on the shop floor at sim_time
Util % = average server utilization
Interpretation
Immediate Release pushes every arrival straight onto the shop floor and completes 2 963 jobs. There is no PSP, so all the queueing happens on the floor: the run ends during a congested spell, End WIP is high (243.2 units of remaining work) and 10.5 % of jobs finish late.
LumsCor holds 49 jobs in the PSP at the end of the simulation. Its workload norm keeps the floor light (End WIP = 52.7 against 243.2 for Immediate) and trims tardiness slightly (8.9 % late, mean tardiness 0.97 vs 1.10). Because it checks periodically (check_timeout=10), a job held in the pool may already be close to or past its due date when it is released, which limits that gain. (LumsCor gate-checks each candidate individually against the workload norm; it is not a true batch burst.)
SLAR keeps lateness much lower (2.5 %) with a mean tardiness of 0.20. It is reactive rather than periodic: it only releases when a server risks starvation or an urgent job needs insertion. That responsiveness gives SLAR the lowest tardiness in the table and, in this run, also the lowest End WIP (49.7, close to LumsCor's 52.7).
When to choose which policy
| Policy | Best for |
|---|---|
| Immediate Release | Baseline; low-utilisation shops where queues stay short naturally |
| LumsCor | Shops with predictable, stable arrival patterns; simple to tune via wl_norm_level |
| SLAR | High-utilisation shops with tight due dates; reacts to starvation and urgency events |
Next steps
- Release control and dispatching — deeper walkthrough of PSP, triggers, and composable policies
- Release Policies API — full parameter reference