Skip to content

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