Skip to content

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 waiting
  • psp.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) -> float used to assign queue priorities (dispatching rules). Lower values are served first. Pass None (default) for FIFO ordering.
  • collect_workload: If True, attaches a CurrentWorkLoadCollector that 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 checks
  • wl_norm_level: Maximum corrected WIP allowed per server
  • allowance_factor: Multiplier for due date slack (higher = more conservative)
  • collect_workload: If True, attaches a CurrentWorkLoadCollector (see ShopFloor extensibility)

Release triggers wired by the builder:

  1. Periodic release (periodic_trigger): every check_timeout time units, release PSP jobs (sorted by planned release date) whose corrected WIP fits within the workload norm.
  2. 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.
  3. 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: If True, attaches a CurrentWorkLoadCollector (see ShopFloor extensibility)

On every job completion at a server, SLAR evaluates three branches in order:

  1. Empty-queue release: if the server queue is empty, immediately release the most urgent PSP candidate (lowest PST) to prevent idling.
  2. 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.
  3. 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_arrival callbacks run synchronously during psp.add(), before the SimPy new_job event fires. Process-based listeners via on_arrival_trigger resume after.
  • shopfloor.on_processing_end callbacks run after the server is released (servers_exit_at is stamped and job.previous_server is available).
  • LUMS COR requires CorrectedWIPStrategy on the shopfloor (set automatically by the builder).
  • SLAR is purely event-driven (no periodic trigger).

Next