Skip to content

API Reference

Auto-generated from docstrings. Lower-level detail; for narrative guides, see the Tutorials.

Release Policies

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.

Requires CorrectedWIPStrategy on the shopfloor — the corrected contribution formula PT / (i + 1) only makes sense under that strategy. The strategy and the norm coverage are checked eagerly at construction.

Example
from simulatte.policies.slar_limit import SlarLimit
from simulatte.shopfloor import CorrectedWIPStrategy

shop_floor.set_wip_strategy(CorrectedWIPStrategy())
slar_limit = SlarLimit(
    shopfloor=shop_floor, psp=psp, router=router,
    wl_norm={s: 5.0 for s in servers},
    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.

Requires CorrectedWIPStrategy on the shopfloor, which accounts for downstream workload when computing WIP at each server.

Example
from simulatte.policies.triggers import periodic_trigger, on_completion_trigger

lumscor = LumsCor(wl_norm={server: 10.0}, allowance_factor=2)
shopfloor.set_wip_strategy(CorrectedWIPStrategy())
psp = PreShopPool(env=env, shopfloor=shopfloor)
env.process(periodic_trigger(psp, 1.0, lumscor.periodic_release))
env.process(on_completion_trigger(shopfloor, psp, lumscor.starvation_release))

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

Dispatching Rules

Tier 1 — stateless rules

shortest_processing_time

Shortest Processing Time at server.

Returns job.routing[server]. Jobs with shorter processing times at the candidate server are served first.

Reference: Conway, Maxwell & Miller (1967), Theory of Scheduling.

earliest_due_date

Earliest Due Date.

Returns job.due_date. Jobs with earlier due dates are served first. server is unused (rule is server-agnostic).

operational_due_date

Operational Due Date at server.

Distributes the planned shop-floor slack across the operations of job's routing so each operation gets an interim due date. Defined as

o_ij = t_r + n_ij * max(0, (d_i - t_r) / |R_i|)

where t_r is the job's shop-floor entry time, n_ij is the static routing-step number of server (1-indexed), d_i is the due date and |R_i| is the fixed routing length at shop-floor entry.

For push systems (no PSP), t_r = job.created_at; for pull systems, t_r = job.psp_exit_at (release time to shop floor).

Reference: Land, Stevenson & Thürer (2014), Integrating load-based order release and priority dispatching, IJPR 52(4), 1059-1073. https://doi.org/10.1080/00207543.2013.836614

Note: this rule assumes each server appears at most once in a job's routing, so .index() always finds the correct step. This holds in practice because ProductionJob.routing is a dict keyed by server, which structurally prevents duplicate entries.

modified_operational_due_date

Modified Operational Due Date at server.

Defined as m_ij = max(o_ij, now + p_ij) where o_ij is the ODD (see operational_due_date) and p_ij = job.routing[server]. Switches dynamically between ODD-driven dispatching (slack-timing regime, when o_ij > now + p_ij) and SPT-driven dispatching (when the job is late w.r.t. its operational due date and the SPT term dominates).

Reference: Baker & Kanet (1983), Job shop scheduling with modified due dates, Journal of Operations Management, 4(1), 11-22. https://doi.org/10.1016/0272-6963(83)90022-0

critical_ratio

Critical Ratio at server.

Defined as cr_ij = (d_i - now) / sum(p_ij for j in R_i) — the ratio of the job's slack time to its remaining processing time. Lower values (jobs that are running out of slack relative to the work left) are served first. R_i is the set of operations not yet completed (i.e. servers not yet exited), including the current one.

Returns inf if the remaining processing time is zero (defensive; a queued job always has at least its current operation pending).

Reference: Berry & Rao (1975), Critical Ratio Scheduling: An Experimental Analysis, Management Science, 22(2), 192-201. https://doi.org/10.1287/mnsc.22.2.192

first_come_first_served

First Come First Served.

Returns 0.0 for every job, so the SimPy key tuple's time component (entry timestamp) does the tiebreaking. Equivalent to the Router's no-rule default but explicit at the call site.

work_in_next_queue

Work In Next Queue (WINQ).

Returns the total processing time of the jobs waiting in the queue of the next machine on job's routing. Jobs whose next machine has less queued work are served first, feeding soon-to-starve downstream machines and adding look-ahead information that SPT lacks.

Queue-only convention: excludes the job currently in service at the next machine. A job on its last operation has no downstream queue and returns 0.0.

Reference: Blackstone, Phillips & Hogg (1982), A state-of-the-art survey of dispatching rules for manufacturing job shop operations, IJPR 20(1), 27-45. https://doi.org/10.1080/00207548208947745

Tier 2 — parameterized rules

planned_slack_time

Build a Planned Slack Time (PST) dispatching rule.

Defined as

pst_ij = (d_i - now) - sum(p_ik + k for k in R_i_from_j)

where R_i_from_j is the set of operations from server through the end of the routing and k is the per-operation queue-time allowance. Lower PST = more urgent (the job is closer to being late).

The returned callable yields inf if server is not in the job's routing or has already been exited — making it safe for priority comparisons and min() calls.

Parameters:

Name Type Description Default
allowance float

Per-operation queue-time allowance k (>= 0). Defaults to 0.0.

0.0

Returns:

Type Description
Callable[[BaseJob, Server], float]

A (job, server) -> float callable suitable for use as a

Callable[[BaseJob, Server], float]

priority_policies on Router or

Callable[[BaseJob, Server], float]

priority_policy on ProductionJob.

Raises:

Type Description
ValueError

If allowance is negative.

Reference: Land & Gaalman (1998), The performance of workload control concepts in job shops: Improving the release method, IJPE 56-57, 347-364. https://doi.org/10.1016/S0925-5273(98)00052-8

slack_per_remaining_operation

Build a Slack per Remaining Operation (S/OPN) dispatching rule.

Defined as sopn_ij = pst_ij(k) / |R_i| where pst_ij is the Planned Slack Time (see planned_slack_time) and |R_i| is the count of operations not yet completed (servers not yet exited), including the current one. Lower S/OPN = more urgent.

The returned callable yields inf if server is not in the job's routing or has already been exited.

Parameters:

Name Type Description Default
allowance float

Per-operation queue-time allowance k (>= 0), forwarded to the underlying PST computation. Defaults to 0.0.

0.0

Returns:

Type Description
Callable[[BaseJob, Server], float]

A (job, server) -> float callable suitable for use as a

Callable[[BaseJob, Server], float]

priority_policies on Router or

Callable[[BaseJob, Server], float]

priority_policy on ProductionJob.

Raises:

Type Description
ValueError

If allowance is negative.

Reference: Kanet (1982), Note—On Anomalies in Dynamic Ratio Type Scheduling Rules: A Clarifying Analysis, Management Science, 28(11), 1337-1341. https://doi.org/10.1287/mnsc.28.11.1337

apparent_tardiness_cost

Build an Apparent Tardiness Cost (ATC) dispatching rule.

Priority index (Vepsäläinen & Morton 1987):

I_j = (w_j / p_j) * exp(-max(0, d_j - p_j - t) / (k * p_bar))

where p_j is the imminent-operation processing time, d_j the due date, t the current time, w_j the job weight, k the look-ahead (scaling) parameter and p_bar the average processing time of the jobs queued at the machine. Higher I_j = more urgent; the returned callable yields -I_j so the lowest key is served first.

The slack uses the imminent operation (d_j - p_j - t), the canonical single-machine Vepsäläinen-Morton form (not a remaining-work or operational due-date variant).

Parameters:

Name Type Description Default
lookahead float

Scaling parameter k (> 0). Vepsäläinen & Morton suggest roughly 1.5-4.5 when slack is tight.

required
avg_processing float | None

Fixed p_bar override. When None (default), p_bar is computed live as the mean imminent processing time of the jobs queued at the server, falling back to p_j when the queue is empty or that mean is non-positive.

None
weight Callable[[BaseJob], float] | None

Optional job -> weight callable. When None, w_j = 1.

None

Returns:

Type Description
Callable[[BaseJob, Server], float]

A (job, server) -> float callable yielding -I_j.

Raises:

Type Description
ValueError

If lookahead <= 0, or avg_processing is given and <= 0.

Reference: Vepsäläinen & Morton (1987), Priority rules for job shops with weighted tardiness costs, Management Science 33(8), 1035-1047. https://doi.org/10.1287/mnsc.33.8.1035

cost_over_time

Build a Cost Over Time (COVERT) dispatching rule.

Priority index:

C_j = w_j * max(0, 1 - max(0, d_j - t - RPT_j) / (k * RPT_j)) / p_j

where RPT_j is the remaining processing time (sum over unfinished_routing, including the current operation), p_j the imminent-operation processing time, d_j the due date, t the current time and k the look-ahead parameter. Higher C_j = more urgent; the returned callable yields -C_j.

Denominator k * RPT_j is the remaining-work waiting allowance (job-shop convention; the single-machine variant uses k * p_j). When the job is tardy or just-in-time (slack <= 0) the rule reduces to a WSPT-like w_j / p_j; when slack >= k * RPT_j the cost is 0.

Parameters:

Name Type Description Default
lookahead float

Look-ahead parameter k (> 0).

required
weight Callable[[BaseJob], float] | None

Optional job -> weight callable. When None, w_j = 1.

None

Returns:

Type Description
Callable[[BaseJob, Server], float]

A (job, server) -> float callable yielding -C_j.

Raises:

Type Description
ValueError

If lookahead <= 0.

Reference: Carroll (1965), Heuristic sequencing of single and multiple component jobs (PhD thesis, MIT). Job-shop form: Russell, Dar-El & Taylor (1987), A comparative analysis of the COVERT job sequencing rule using various shop performance measures, IJPR 25(10), 1523-1540.

raghu_rajendran

Build a Raghu & Rajendran (RR) dispatching rule.

Priority index (Raghu & Rajendran 1993):

Z_j = exp(u) * p_j + (s_j / RPT_j) * exp(-u) * p_j + WINQ_j

where p_j is the imminent-operation processing time, u the current machine's utilization, s_j = d_j - RPT_j - t the raw slack (may be negative), RPT_j the remaining processing time (sum over unfinished_routing) and WINQ_j the work content in the next machine's queue. RR is a minimum-Z rule, so the index is returned directly (lowest served first, no negation).

The exponential weighting of the processing-time and due-date terms by the machine's own utilization is RR's defining feature: the balance differs machine to machine. A negative s_j (tardy job) lowers Z_j, giving tardy jobs strong priority.

Parameters:

Name Type Description Default
utilization float | None

Fixed machine utilization u override in [0, 1]. When None (default), u is read live from server.utilization_rate; early in a run this is ~= 0, where exp(0) = 1 degrades the rule gracefully to p_j + (s_j / RPT_j) * p_j + WINQ_j.

None

Returns:

Type Description
Callable[[BaseJob, Server], float]

A (job, server) -> float callable yielding Z_j.

Raises:

Type Description
ValueError

If utilization is given and outside [0, 1].

Reference: Raghu & Rajendran (1993), An efficient dynamic dispatching rule for scheduling in a job shop, IJPE 32(3), 301-313. https://doi.org/10.1016/0925-5273(93)90044-L

Tier 3 — system-state rules

Focus

FOCUS dispatching rule — weighted combination of five impact mechanisms.

The class is stateless beyond its weights. Computations are organised so each mechanism (pi, omega, psi, gamma, beta) is exposed independently for testability and for use as a building block by higher-level policies (DRACO).

All five mechanisms return values in [0, 1] with 1 indicating a "relevant" impact and 0 an "irrelevant" one. The aggregated score is the weighted average of the five pieces and also lies in [0, 1].

Design note: unlike the stateless dispatching-rule factories (e.g. planned_slack_time), FOCUS is a class because it exposes each mechanism as an independently testable method and a shared per-decision build_context consumed by higher-level policies (DRACO). A bare (job, server) -> float closure cannot expose these. Use FocusPriorityRule to adapt a Focus to the priority_policy contract.

Parameters:

Name Type Description Default
weights tuple[float, float, float, float, float]

(w1, w2, w3, w4, w5) for the five mechanisms; must each be in [0, 1] and sum to 1 (within floating-point tolerance). A zero weight disables the corresponding mechanism. Defaults to (0.25, 0.25, 0.25, 0.25, 0.0) — beta dormant, preserving the original four-mechanism behaviour.

Weight ordering (IMPORTANT). The tuple follows the DRACO paper's Eq-9 four-mechanism order with beta appended 5th — (π, ξ, τ, δ, β) — NOT the FOCUS paper's Eq-12 order (π, β, ξ, τ, δ). Index → mechanism → method → FOCUS Eq-12 slot::

w1 → SPT            → pi    → π   (FOCUS Eq-12 w1)
w2 → starvation     → omega → ξ   (FOCUS Eq-12 w3)
w3 → slack timing   → psi   → τ   (FOCUS Eq-12 w4)
w4 → pacing         → gamma → δ   (FOCUS Eq-12 w5)
w5 → WIP balancing  → beta  → β   (FOCUS Eq-12 w2)

The default and the all-equal baseline are order-invariant, but reproducing the Omega paper's per-mechanism ablations requires translating the index — copying the paper's "set wᵢ = 0" verbatim zeroes the wrong mechanism here. To reproduce each ablation (the removed mechanism's weight is 0, the rest 1/4)::

FOCUS-π : (0.0,  0.25, 0.25, 0.25, 0.25)
FOCUS-β : (0.25, 0.25, 0.25, 0.25, 0.0 )   # the default
FOCUS-ξ : (0.25, 0.0,  0.25, 0.25, 0.25)
FOCUS-τ : (0.25, 0.25, 0.0,  0.25, 0.25)
FOCUS-δ : (0.25, 0.25, 0.25, 0.0,  0.25)

The full all-five FOCUS is (0.2, 0.2, 0.2, 0.2, 0.2). Do not reorder the tuple: DRACO, the default, and every caller depend on the current order.

(0.25, 0.25, 0.25, 0.25, 0.0)

Example (inside DRACO — one ctx, many candidates): >>> focus = Focus(weights=(0.2, 0.2, 0.2, 0.2, 0.2)) >>> ctx = focus.build_context(shopfloor, env.now, psp=psp) >>> for candidate in candidates: ... d_score = focus.score(candidate, server_k, ctx, env.now)

FocusContext

Snapshot of shop-wide aggregates at a single decision instant.

Built once per decision via Focus.build_context and reused across all candidates being scored at that instant. Computing this object is O(|O| · |J|) (the |J| factor comes from the beta entropy pass; without beta the cost is O(|O|)).

Attributes:

Name Type Description
max_pij float

Max processing time over all pending (i, j) pairs in the shop (the set P in the FOCUS paper, Eq 1). 0 if no pending ops.

empty_queue_servers frozenset[Server]

Servers whose queue is empty at the snapshot instant.

max_positive_slack float

Max of S_i across all jobs in O with positive slack; 0 if no positive-slack jobs.

max_positive_pacing float

Max of V_i = S_i / |R_i| across all jobs in O with positive V_i; 0 if none.

workloads tuple[float, ...]

Per-server workload W_j = sum p_xj over jobs in server.queue ∪ server.users (full processing time). Indexed by server_index[server].

server_index Mapping[Server, int]

Read-only mapping Server -> index into workloads. Wrapped in types.MappingProxyType so shallow mutation of the index is detected at runtime. Tightly coupled with workloads; same lifetime.

pre_entropy float

Shop-wide workload entropy at the snapshot instant (e_minus in the beta spec). See _entropy for the empty-shop convention.

max_positive_c float

Max of c(i) = e(i) - pre_entropy across all jobs i in O with c(i) > 0; 0 if no improving dispatch exists. Beta's normalizer.

c_values Mapping[BaseJob, float]

Read-only mapping job -> c(i) for every job in O with remaining ops, computed in the beta pass at the job's first uncompleted server. Empty when compute_beta=False. Lets beta reuse the per-job entropy delta instead of recomputing it.

FocusPriorityRule

Adapter exposing Focus as a simulatte priority_policy.

Wraps a Focus and a ShopFloor into a (job, server) -> float callable suitable for simulatte.router.Router.priority_policies. The returned value is the negated FOCUS score because simulatte's simpy.resources.resource.PriorityResource sorts ascending (lower key = served first).

Liveness guarantee

simulatte.server.Server.sort_queue re-evaluates priority_policy for every queued request before every dispatch event (auto-called by _trigger_put). The context is memoized within a single sort_queue pass (same now and frozen shop state → one build reused across all requests in that pass) and rebuilt whenever the scanned shop state changes, so the key returned at dispatch time always reflects current shop aggregates — no external refresh helper is needed.

Parameters:

Name Type Description Default
focus Focus

A Focus instance.

required
shopfloor ShopFloor

The shopfloor against which ctx is built per call.

required
psp PreShopPool | None

Optional PreShopPool; when provided its jobs are included in the O aggregate (so PSP candidates show up in FOCUS aggregates even before release).

None