Skip to content

Commit 772d001

Browse files
GiggleLiuisPANNclaude
authored
Fix #510: [Model] JobShopScheduling (#760)
* Add plan for #510: [Model] JobShopScheduling * Implement #510: [Model] JobShopScheduling * chore: remove plan file after implementation * fix: adapt JobShopScheduling to Value trait (Metric→Value rename) - Replace `type Metric = bool` with `type Value = Or` and return `Or(...)` from evaluate - Remove obsolete `SatisfactionProblem` import and `sat` keyword in declare_variants! - Update tests to use `Or(true)`/`Or(false)` assertions and `find_witness` - Fix CLI help text test to include JobShopScheduling in num-processors list - Remove incorrect test for ExpectedRetrievalCost latency_bound requirement Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: convert JobShopScheduling from satisfaction to optimization - Change Value from Or to Min<u64> (minimize makespan) - Remove deadline field — optimization finds the minimum makespan directly - Update evaluate() to return Min(Some(makespan)) or Min(None) - Make schedule_from_config() public for test access - Add CLI validation for consecutive-processor constraint - Add test for consecutive-processor rejection - Update paper definition and figure (remove deadline references) - Update canonical example optimal_value from true to 19 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * simplify: eliminate double flatten_tasks() and redundant resolve_alias - Refactor evaluate() to call flatten_tasks() once, reuse for both schedule_from_config_inner() and inline makespan computation - Remove redundant manual resolve_alias entry for JobShopScheduling (registry fallback already handles case-insensitive lookup) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: extract shared Lehmer-code decode utility across 7 models Extract `decode_lehmer()` and `lehmer_dims()` into `models::misc` and replace duplicated Lehmer-code decode logic in all 7 scheduling models: - FlowShopScheduling - JobShopScheduling - MinimumTardinessSequencing - SequencingToMinimizeMaximumCumulativeCost - SequencingToMinimizeWeightedCompletionTime - SequencingToMinimizeWeightedTardiness - SequencingWithReleaseTimesAndDeadlines Net: +46 / -109 lines. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: derive paper Gantt chart from examples.json, remove hardcoded data Replace hardcoded blocks/makespan in the JobShopScheduling paper entry with Typst computation that decodes the Lehmer config, runs topo-sort longest-path scheduling, and builds Gantt blocks from examples.json. Also derive job descriptions and machine count from the data. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Xiwei Pan <xiwei.pan@connect.hkust-gz.edu.cn> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 704ed6a commit 772d001

16 files changed

Lines changed: 802 additions & 115 deletions

docs/paper/reductions.typ

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@
159159
"IntegralFlowWithMultipliers": [Integral Flow With Multipliers],
160160
"MinMaxMulticenter": [Min-Max Multicenter],
161161
"FlowShopScheduling": [Flow Shop Scheduling],
162+
"JobShopScheduling": [Job-Shop Scheduling],
162163
"GroupingBySwapping": [Grouping by Swapping],
163164
"MinimumCutIntoBoundedSets": [Minimum Cut Into Bounded Sets],
164165
"MinimumDummyActivitiesPert": [Minimum Dummy Activities in PERT Networks],
@@ -5122,6 +5123,163 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
51225123
]
51235124
}
51245125

5126+
#{
5127+
let x = load-model-example("JobShopScheduling")
5128+
let jobs = x.instance.jobs
5129+
let m = x.instance.num_processors
5130+
let n = jobs.len()
5131+
let lehmer = x.optimal_config
5132+
5133+
// Flatten tasks: build per-machine task lists and lengths
5134+
let task-lengths = ()
5135+
let task-job = () // which job each flat task belongs to
5136+
let task-index = () // which task within the job
5137+
let machine-tasks = range(m).map(_ => ())
5138+
let tid = 0
5139+
for (ji, job) in jobs.enumerate() {
5140+
for (ki, op) in job.enumerate() {
5141+
let (mi, len) = op
5142+
task-lengths.push(len)
5143+
task-job.push(ji)
5144+
task-index.push(ki)
5145+
machine-tasks.at(mi).push(tid)
5146+
tid += 1
5147+
}
5148+
}
5149+
let T = task-lengths.len()
5150+
5151+
// Decode per-machine Lehmer codes into machine orders
5152+
let offset = 0
5153+
let machine-orders = ()
5154+
for mi in range(m) {
5155+
let mt = machine-tasks.at(mi)
5156+
let k = mt.len()
5157+
let seg = lehmer.slice(offset, offset + k)
5158+
let avail = range(k)
5159+
let order = ()
5160+
for c in seg {
5161+
order.push(mt.at(avail.at(c)))
5162+
avail = avail.enumerate().filter(((i, v)) => i != c).map(((i, v)) => v)
5163+
}
5164+
machine-orders.push(order)
5165+
offset += k
5166+
}
5167+
5168+
// Build DAG edges (job precedence + machine order)
5169+
let successors = range(T).map(_ => ())
5170+
let indegree = range(T).map(_ => 0)
5171+
// Job precedence edges
5172+
let job-task-start = 0
5173+
for job in jobs {
5174+
for i in range(job.len() - 1) {
5175+
let u = job-task-start + i
5176+
let v = job-task-start + i + 1
5177+
successors.at(u).push(v)
5178+
indegree.at(v) += 1
5179+
}
5180+
job-task-start += job.len()
5181+
}
5182+
// Machine order edges
5183+
for order in machine-orders {
5184+
for i in range(order.len() - 1) {
5185+
let u = order.at(i)
5186+
let v = order.at(i + 1)
5187+
successors.at(u).push(v)
5188+
indegree.at(v) += 1
5189+
}
5190+
}
5191+
5192+
// Topological sort + longest-path to compute start times
5193+
let start-times = range(T).map(_ => 0)
5194+
let queue = ()
5195+
for t in range(T) {
5196+
if indegree.at(t) == 0 { queue.push(t) }
5197+
}
5198+
while queue.len() > 0 {
5199+
let u = queue.remove(0)
5200+
let finish = start-times.at(u) + task-lengths.at(u)
5201+
for v in successors.at(u) {
5202+
if finish > start-times.at(v) { start-times.at(v) = finish }
5203+
indegree.at(v) -= 1
5204+
if indegree.at(v) == 0 { queue.push(v) }
5205+
}
5206+
}
5207+
5208+
// Build Gantt blocks: (machine, job, task-within-job, start, end)
5209+
let blocks = ()
5210+
for t in range(T) {
5211+
let (mi, _len) = jobs.at(task-job.at(t)).at(task-index.at(t))
5212+
blocks.push((mi, task-job.at(t), task-index.at(t), start-times.at(t), start-times.at(t) + task-lengths.at(t)))
5213+
}
5214+
let makespan = calc.max(..range(T).map(t => start-times.at(t) + task-lengths.at(t)))
5215+
[
5216+
#problem-def("JobShopScheduling")[
5217+
Given a positive integer $m$, a set $J$ of jobs, where each job $j in J$ consists of an ordered list of tasks $t_1[j], dots, t_(n_j)[j]$ with processor assignments $p(t_k[j]) in {1, dots, m}$, processing lengths $ell(t_k[j]) in ZZ^+_0$, and consecutive-processor constraint $p(t_k[j]) != p(t_(k+1)[j])$, find start times $sigma(t_k[j]) in ZZ^+_0$ such that tasks sharing a processor do not overlap, each job respects $sigma(t_(k+1)[j]) >= sigma(t_k[j]) + ell(t_k[j])$, and the makespan $max_(j in J) (sigma(t_(n_j)[j]) + ell(t_(n_j)[j]))$ is minimized.
5218+
][
5219+
Job-Shop Scheduling is the classical disjunctive scheduling problem SS18 in Garey & Johnson; Garey, Johnson, and Sethi proved it strongly NP-hard already for two machines @garey1976. Unlike Flow Shop Scheduling, each job carries its own machine route, so the difficulty lies in choosing a compatible relative order on every machine and then finding the schedule with minimum makespan. This implementation follows the original Garey-Johnson formulation, including the requirement that consecutive tasks of the same job use different processors, and evaluates a witness by orienting the machine-order edges and propagating longest paths through the resulting precedence DAG. The registered baseline therefore exposes a factorial upper bound over task orders#footnote[The auto-generated complexity table records the concrete upper bound used by the Rust implementation; no sharper exact bound is cited here.].
5220+
5221+
*Example.* The canonical fixture has #m machines and #n jobs
5222+
$
5223+
#for (ji, job) in jobs.enumerate() {
5224+
$J_#(ji+1) = (#job.map(((mi, len)) => $(M_#(mi+1), #len)$).join($,$))$
5225+
if ji < n - 1 [$,$] else [.]
5226+
}
5227+
$
5228+
The witness stored in the example DB orders the six tasks on $M_1$ as $(J_1^1, J_2^2, J_3^1, J_4^2, J_5^1, J_5^3)$ and the six tasks on $M_2$ as $(J_2^1, J_4^1, J_1^2, J_3^2, J_5^2, J_2^3)$. Taking the earliest schedule consistent with those machine orders yields the Gantt chart in @fig:jobshop, whose makespan is $#makespan$.
5229+
5230+
#pred-commands(
5231+
"pred create --example " + problem-spec(x) + " -o job-shop-scheduling.json",
5232+
"pred solve job-shop-scheduling.json --solver brute-force",
5233+
"pred evaluate job-shop-scheduling.json --config " + x.optimal_config.map(str).join(","),
5234+
)
5235+
5236+
#figure(
5237+
canvas(length: 1cm, {
5238+
import draw: *
5239+
let colors = (rgb("#4e79a7"), rgb("#e15759"), rgb("#76b7b2"), rgb("#f28e2b"), rgb("#59a14f"))
5240+
let scale = 0.38
5241+
let row-h = 0.6
5242+
let gap = 0.15
5243+
5244+
for mi in range(m) {
5245+
let y = -mi * (row-h + gap)
5246+
content((-0.8, y), text(8pt, "M" + str(mi + 1)))
5247+
}
5248+
5249+
for block in blocks {
5250+
let (mi, ji, ti, s, e) = block
5251+
let x0 = s * scale
5252+
let x1 = e * scale
5253+
let y = -mi * (row-h + gap)
5254+
rect(
5255+
(x0, y - row-h / 2),
5256+
(x1, y + row-h / 2),
5257+
fill: colors.at(ji).transparentize(30%),
5258+
stroke: 0.4pt + colors.at(ji),
5259+
)
5260+
content(((x0 + x1) / 2, y), text(6pt, "j" + str(ji + 1) + "." + str(ti + 1)))
5261+
}
5262+
5263+
let y-axis = -(m - 1) * (row-h + gap) - row-h / 2 - 0.2
5264+
line((0, y-axis), (makespan * scale, y-axis), stroke: 0.4pt)
5265+
for t in range(calc.ceil(makespan / 5) + 1).map(i => calc.min(i * 5, makespan)) {
5266+
let x = t * scale
5267+
line((x, y-axis), (x, y-axis - 0.1), stroke: 0.4pt)
5268+
content((x, y-axis - 0.25), text(6pt, str(t)))
5269+
}
5270+
if calc.rem(makespan, 5) != 0 {
5271+
let x = makespan * scale
5272+
line((x, y-axis), (x, y-axis - 0.1), stroke: 0.4pt)
5273+
content((x, y-axis - 0.25), text(6pt, str(makespan)))
5274+
}
5275+
content((makespan * scale / 2, y-axis - 0.5), text(7pt)[$t$])
5276+
}),
5277+
caption: [Job-shop schedule induced by the canonical machine-order witness. The optimal makespan is #makespan.],
5278+
) <fig:jobshop>
5279+
]
5280+
]
5281+
}
5282+
51255283
#problem-def("StaffScheduling")[
51265284
Given a collection $C$ of binary schedule patterns of length $m$, where each pattern has exactly $k$ ones, a requirement vector $overline(R) in ZZ_(>= 0)^m$, and a worker budget $n in ZZ_(>= 0)$, determine whether there exists a function $f: C -> ZZ_(>= 0)$ such that $sum_(c in C) f(c) <= n$ and $sum_(c in C) f(c) dot c >= overline(R)$ component-wise.
51275285
][

problemreductions-cli/src/cli.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ Flags by problem type:
298298
PartiallyOrderedKnapsack --sizes, --values, --capacity, --precedences
299299
QAP --matrix (cost), --distance-matrix
300300
StrongConnectivityAugmentation --arcs, --candidate-arcs, --bound [--num-vertices]
301+
JobShopScheduling --job-tasks [--num-processors]
301302
FlowShopScheduling --task-lengths, --deadline [--num-processors]
302303
StaffScheduling --schedules, --requirements, --num-workers, --k
303304
TimetableDesign --num-periods, --num-craftsmen, --num-tasks, --craftsman-avail, --task-avail, --requirements
@@ -643,10 +644,13 @@ pub struct CreateArgs {
643644
/// Task lengths for FlowShopScheduling (semicolon-separated rows: "3,4,2;2,3,5;4,1,3")
644645
#[arg(long)]
645646
pub task_lengths: Option<String>,
647+
/// Job tasks for JobShopScheduling (semicolon-separated jobs, comma-separated processor:length tasks, e.g., "0:3,1:4;1:2,0:3,1:2")
648+
#[arg(long)]
649+
pub job_tasks: Option<String>,
646650
/// Deadline for FlowShopScheduling, MultiprocessorScheduling, or ResourceConstrainedScheduling
647651
#[arg(long)]
648652
pub deadline: Option<u64>,
649-
/// Number of processors/machines for FlowShopScheduling, MultiprocessorScheduling, ResourceConstrainedScheduling, or SchedulingWithIndividualDeadlines
653+
/// Number of processors/machines for FlowShopScheduling, JobShopScheduling, MultiprocessorScheduling, ResourceConstrainedScheduling, or SchedulingWithIndividualDeadlines
650654
#[arg(long)]
651655
pub num_processors: Option<usize>,
652656
/// Binary schedule patterns for StaffScheduling (semicolon-separated rows, e.g., "1,1,0;0,1,1")
@@ -883,7 +887,7 @@ mod tests {
883887
));
884888
assert!(
885889
help.contains(
886-
"Number of processors/machines for FlowShopScheduling, MultiprocessorScheduling, ResourceConstrainedScheduling, or SchedulingWithIndividualDeadlines"
890+
"Number of processors/machines for FlowShopScheduling, JobShopScheduling, MultiprocessorScheduling, ResourceConstrainedScheduling, or SchedulingWithIndividualDeadlines"
887891
),
888892
"create help should describe --num-processors for both scheduling models"
889893
);

0 commit comments

Comments
 (0)