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 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. Target utilisation ≈ 87–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

import random

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."""
    random.seed(seed)
    with Environment() 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   3026    0         11.8%    1.04        19.93       90.6       87.7%  
LumsCor     3018    23        11.6%    1.76        20.67       26.2       87.6%  
SLAR        3025    17        1.0%     0.07        19.79       45.5       87.8%  

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 all 3 026 jobs straight onto the shop floor. There is no PSP, so End WIP is high (90.6 units of remaining work) and 11.8 % of jobs finish late.

LumsCor holds 23 jobs in the PSP at end of simulation. Its workload norm keeps queues short (End WIP = 26.2, the lowest in the table), but because it checks periodically (check_timeout=10), the PSP waiting time it introduces means jobs held in the pool may already be at or past their due date when released — adding tardiness (1.76 vs 1.04 for Immediate) without a corresponding WIP benefit. (LumsCor gate-checks each candidate individually against the workload norm; it is not a true batch burst.)

SLAR keeps late-ness dramatically lower (1.0 %) with a mean tardiness of just 0.07. 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, while its End WIP (45.5) sits between Immediate (90.6) and LumsCor (26.2) — SLAR trades tighter WIP control for better due-date performance.

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