Skip to content

Release Policies

Release policies control when jobs are released from the pre-shop pool onto the shop floor, regulating work-in-process (WIP) and shop congestion. Simulatte ships several workload-control policies — Slar, SlarLimit, LumsCor, Draco, ConWIP, and ContinuousRelease — along with event triggers and a starvation-avoidance callback. For the underlying concepts and a worked walkthrough, see the Production Planning & Control guide and the Release control and dispatching tutorial.

Slar

Superfluous Load Avoidance Release (SLAR) policy.

Implements the SLAR algorithm from Land & Gaalman (1998) extended with a starvation-avoidance sub-trigger. On every job-completion event at a server, _consider_release evaluates three branches (in order):

  1. Idle prevention (paper rule): if the server queue is empty, release the most urgent PSP candidate (lowest PST) to prevent the server from idling.
  2. Urgent insertion (paper rule): if no queued job is urgent (negative PST) but the PSP holds an urgent candidate, release the urgent candidate with the shortest processing time to minimise disruption to the queue. Skipped if an urgent job is already in the queue — the priority rule will dispatch it next.
  3. Drain safety net (extension): if exactly one job remains in the queue, schedule a postponed release of the most urgent PSP candidate so the queue is replenished before it drains. Mutually exclusive with (2): if (2) fires, (3) does not, to avoid superfluous load.

Construction is active: the instance self-registers with shopfloor.on_processing_end and psp.on_arrival(starvation_avoidance), and (if a router is provided) sets router.priority_policies to the PST dispatching rule. This makes it impossible to forget the priority wiring that the algorithm depends on.

Example
slar = Slar(
    shopfloor=shop_floor, psp=psp, router=router,
    allowance_factor=3.0,
)
Reference

Land, M.J. & Gaalman, G.J.C. (1998). The performance of workload control concepts in job shops: Improving the release method. International Journal of Production Economics, 56-57, 347-364. https://doi.org/10.1016/S0925-5273(98)00052-8

SlarLimit

Bases: Slar

SLAR augmented with a workload-norm limit on urgent insertion.

Identical to Slar except for the urgent-insertion branch:

  • Classic SLAR releases the urgent PSP candidate (negative PST) with the shortest processing time, unconditionally.
  • SLAR-Limit iterates urgent PSP candidates in ascending SPT order and releases the first whose corrected workload contribution PT / (i + 1) keeps every server in its routing at or below its configured norm. If no urgent candidate fits, the drain safety-net branch may still fire — same as in Slar.

The idle-prevention and drain-safety-net branches, the PST priority rule, and the postponed-release mechanism are inherited unchanged.

The corrected contribution formula PT / (i + 1) only makes sense under CorrectedWIPStrategy. Construction is active: the instance sets CorrectedWIPStrategy on the shopfloor (so the formula applies) and validates the norm coverage eagerly.

Example
from simulatte.policies.slar_limit import SlarLimit

slar_limit = SlarLimit(
    shopfloor=shop_floor, psp=psp, router=router,
    wl_norm=5.0,  # scalar norm, expanded to every server (a dict is also accepted)
    allowance_factor=3.0,
)
Reference

Thürer, M. & Stevenson, M. (2021). Improving superfluous load avoidance release (SLAR): A new load-based SLAR mechanism. International Journal of Production Economics, 231, 107881. https://doi.org/10.1016/j.ijpe.2020.107881

LumsCor

Workload-based release policy using corrected WIP and planned release dates.

LUMS-COR controls job releases by: 1. Sorting PSP jobs by planned release date (earliest first) 2. Releasing a job only if adding it keeps each server's corrected WIP at or below its workload norm

The starvation release complements periodic releases by immediately releasing jobs when servers become idle or nearly idle.

Construction is active (like Slar): the instance sets CorrectedWIPStrategy on the shopfloor, the PST priority rule on the router, a periodic release trigger, a completion-triggered starvation release, and starvation_avoidance on PSP arrival.

Example
lumscor = LumsCor(
    shopfloor=shopfloor, psp=psp, router=router,
    wl_norm=10.0, check_timeout=1.0, allowance_factor=2,
)

Draco

Non-hierarchical DRACO release/dispatch policy.

Design note: DRACO is a class (not a dispatching-rule factory) because it holds shop-coupled state (the WIP target, per-pair loop targets, the embedded Focus, and the one-shot force flags) and exposes both a priority_policy and a decide_next_job (on_processing_end) callback.

Trigger: shopfloor.on_processing_end — the same mechanism SLAR uses. On every job exit from any server k, decide_next_job runs (with k passed directly by the callback) and selects the next job to be processed at k from Q_k ∪ P_k.

Active construction (like Slar.__init__): the instance self-wires every hook it depends on — it sets router.priority_policies to its priority_policy, registers decide_next_job on shopfloor.on_processing_end, and (when a psp is given) registers starvation_avoidance on psp.on_arrival. So build_draco_system stays a single Draco(...) line and a user wiring DRACO by hand cannot forget any of the three.

Strict paper semantics — "the winner is the next processed": DRACO scores Q_k ∪ P_k once per completion and the winner must be the next job processed at k. But priority_policy re-derives queue-side scores live on every sort_queue — and for multi-op jobs the queue-side context can shift between the decision and the dispatch (see Decision instant below) — so the order sort_queue produces is not guaranteed to match DRACO's decision. To make the decision authoritative, DRACO sets a _forced_at_server flag for the winner — whether it came from Q_k or P_k — and priority_policy returns -inf for that job at that server while the flag is set, guaranteeing queue[0] = winner across every sort_queue re-evaluation. A PSP winner additionally gets released onto the shop floor; a queue winner is already in Q_k and only needs the pin. The flag is cleared at the START of the next decide_next_job call for the same server (not on first read), so repeated _trigger_put sorts cannot wipe it out. (Correctness assumes a single freed slot per completion, i.e. capacity == 1, as in build_draco_system and the paper.)

Timing — why this is correct without any shopfloor.py changes: decide_next_job is wired via shopfloor.on_processing_end, so it runs synchronously inside shopfloor.main the moment the with server.request() block exits. By then server.release(req) has already removed the request from users (so count drops to 0) and scheduled a Release event (NORMAL), but that event has not yet been dequeued and no SimPy process-based listener has resumed. If DRACO releases a PSP winner via psp.shopfloor.add, the new process's Initialize event is URGENT (simpy/events.py) and therefore runs before the pending Release event; inside the new process, server.request calls _trigger_put synchronously, finds users empty, and grants the slot immediately. The Release event then fires with nothing to dispatch. Net result: the PSP winner has the server. For a queue winner there is no new process; that same Release event's _trigger_put calls sort_queue, which sees the forced -inf and pins the winner at queue[0] before granting the slot. Either way the callback fires before any event is dequeued, so correctness rests on event priority — URGENT Initialize before NORMAL Release for a PSP winner, and the live -inf for a queue winner — never on same-priority event-id ordering.

Decision instant: the callback fires before the just-finished job re-enters its next server's queue, so a multi-op triggering job is not part of the shop scan at decision time. Because DRACO force-pins the winner, dispatch follows the decision verbatim — there is no later sort_queue re-derivation to disagree with — so the R, A and D terms are all evaluated consistently at this single instant. (Whether the in-transit order should be counted at the decision instant is a literature-faithfulness question, of the same flavour as the build_context note on the O set; the consistency of decision and dispatch does not depend on the answer.)

Cold start / bootstrapping: DRACO's decision is triggered only on job completions. In an idle or lightly loaded shop, no completion fires, so an arriving job would sit in the PSP indefinitely. build_draco_system therefore also wires psp.on_arrival(starvation_avoidance): when a new arrival's first server is completely idle, the job is released immediately, bypassing the R/A/D scoring. This is a liveness provision, not a DRACO decision — in steady state, completion-triggered decisions dominate. (Faithfulness of this provision to Kasper et al. 2023 has not been verified against the primary source.)

Downstream auto-grant (no decision on routing-in): decide_next_job fires only on completion at a server. When a released job routes into an idle downstream server B while a P_B pool candidate waits, SimPy grants B to the arriving job immediately and DRACO never weighs pulling the pool candidate — a decision moment in the paper's sense passes without R/A/D scoring. Rare at the paper's ~90% utilization; not corrected here, noted for faithfulness (review §6.F).

Parameters:

Name Type Description Default
shopfloor ShopFloor

The shopfloor against which DRACO's contexts and count-WIP are computed. Required (unlike SLAR which is stateless against shop state). decide_next_job is registered on its on_processing_end.

required
router Router

The router whose priority_policies is set to priority_policy. Required: DRACO's queue ordering depends on its score being applied to every job. (priority_policies is read at job-creation time, so assigning it post-construction is sufficient.)

required
focus_weights tuple[float, float, float, float, float]

(w1, w2, w3, w4, w5) for FOCUS's five pieces.

(0.25, 0.25, 0.25, 0.25, 0.0)
total_impact_weights tuple[float, float, float]

(w^R, w^A, w^D), must sum to 1. Defaults to (0.25, 0.25, 0.5) — the paper's full DRACO configuration (Table 2: W^R = W^A = 1/4, W^D = 1/2).

(0.25, 0.25, 0.5)
wip_target int

Target shop WIP τ (count of jobs). Spec §3.1.

required
loop_target int | dict[tuple[Server, Server], int]

Target overlapping loop ε_{k,u}. Spec §3.2. Accepts a scalar (applied to every pair) or a dict[(k, u), int] mapping for per-pair targets.

required
psp PreShopPool | None

Optional PreShopPool. When provided, its jobs are included in the O aggregate that FOCUS uses (so PSP candidates are reflected in shop-wide aggregates like max p_ij and max S_i), and starvation_avoidance is registered on its on_arrival so a job whose first server is idle is released immediately (a cold-start liveness provision). Optional because some test setups don't have a PSP wired up.

None
References

Kasper, A., Land, M., Teunter, R. (2023). Non-hierarchical work-in-progress control in manufacturing. International Journal of Production Economics, 257, 108768. https://doi.org/10.1016/j.ijpe.2022.108768

ConWIP

ConWIP (Constant Work-In-Process) order release.

Maintains a shop-wide WIP cap (job count). Jobs are released from PSP whenever the shopfloor job count is below the cap. Selection by EDD.

Two triggers are provided:

  • on_completion_release: Wired via on_completion_trigger. When a job finishes processing, releases PSP jobs (by EDD) until the WIP cap is reached.

  • on_arrival_release: Wired via psp.on_arrival(). When a job enters the PSP, immediately releases it if shop WIP is under the cap.

Construction is active (like Slar): the instance wires a completion-triggered release and on_arrival_release on PSP arrival.

Example
conwip = ConWIP(shopfloor=shopfloor, psp=psp, wip_cap=12)

ContinuousRelease

Continuous workload-controlled order release.

Jobs may be released at any moment when a server's corrected workload drops below its norm. On arrival, jobs are released to idle first servers if norms permit (prevents empty-system deadlock).

Uses corrected aggregate load: contribution at position i = PT / (i + 1). Requires CorrectedWIPStrategy on shopfloor.

Two triggers are provided:

  • on_completion_release: Wired via on_completion_trigger. When a job finishes processing, releases PSP jobs (sorted by planned release date) whose corrected WIP fits within norms.

  • on_arrival_release: Wired via psp.on_arrival(). When a job enters the PSP, immediately releases it if its first server is idle and norms permit.

Construction is active (like Slar): the instance sets CorrectedWIPStrategy on the shopfloor, a completion-triggered release, and on_arrival_release on PSP arrival.

Example
cr = ContinuousRelease(shopfloor=shopfloor, psp=psp, wl_norm=5.0)

Triggers

Invoke a release function when a new job arrives in the PSP.

This trigger runs continuously, waiting for the PSP's new_job event. When a job arrives, the release function is called with that job, allowing immediate release decisions based on job properties.

Parameters:

Name Type Description Default
psp PreShopPool

The Pre-Shop Pool to monitor.

required
release_fn Callable[[ProductionJob, PreShopPool], None]

Function called when a job arrives. Signature: (job: ProductionJob, psp: PreShopPool) -> None

required

Yields:

Type Description
ProcessGenerator

The PSP's new_job event repeatedly.

Example

def release_if_server_empty(job, psp): ... if job.servers[0].empty: ... psp.remove(job=job) ... psp.shopfloor.add(job) env.process(on_arrival_trigger(psp, release_if_server_empty))

Invoke a release function when any job completes processing at a server.

This trigger runs continuously, waiting for the shopfloor's job_processing_end event. When a job finishes an operation, the release function is called, enabling starvation avoidance and load-based release.

Parameters:

Name Type Description Default
shopfloor ShopFloor

The shopfloor to monitor for job completions.

required
psp PreShopPool

The Pre-Shop Pool containing candidate jobs for release.

required
release_fn Callable[[ProductionJob, PreShopPool], None]

Function called when a job completes processing. Signature: (triggering_job: ProductionJob, psp: PreShopPool) -> None The triggering_job is the job that just finished processing.

required

Yields:

Type Description
ProcessGenerator

The shopfloor's job_processing_end event repeatedly.

Example

def release_on_starvation(triggering_job, psp): ... server = triggering_job.previous_server ... if server and server.empty: ... candidate = next((j for j in psp.jobs if j.starts_at(server)), None) ... if candidate: ... psp.remove(job=candidate) ... psp.shopfloor.add(candidate) env.process(on_completion_trigger(shop_floor, psp, release_on_starvation))

Invoke a release function at regular intervals.

This trigger runs continuously, waiting for the specified interval before invoking the release function. The function is only called if the PSP is non-empty.

Parameters:

Name Type Description Default
psp PreShopPool

The Pre-Shop Pool to monitor.

required
interval float

Time between release attempts in simulation time units.

required
release_fn Callable[[PreShopPool], None]

Function that examines the PSP and releases jobs. Signature: (psp: PreShopPool) -> None

required

Yields:

Type Description
ProcessGenerator

SimPy timeout events at the specified interval.

Example

def release_all(psp): ... while not psp.empty: ... job = psp.remove() ... psp.shopfloor.add(job) env.process(periodic_trigger(psp, 1.0, release_all))

Starvation avoidance

Starvation avoidance mechanism for PSP-controlled systems.

This module provides a callback that prevents server starvation in pull-based release systems (LumsCor, SLAR). When registered via psp.on_arrival(), it immediately releases any arriving job whose first server is idle, bypassing the normal release policy to ensure servers don't sit idle unnecessarily.