From 162ff2a81b81a8863a7233259a9c29720b694e99 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 23 Mar 2026 16:34:19 +0800 Subject: [PATCH 1/8] Add plan for #510: [Model] JobShopScheduling --- docs/plans/2026-03-23-job-shop-scheduling.md | 295 +++++++++++++++++++ 1 file changed, 295 insertions(+) create mode 100644 docs/plans/2026-03-23-job-shop-scheduling.md diff --git a/docs/plans/2026-03-23-job-shop-scheduling.md b/docs/plans/2026-03-23-job-shop-scheduling.md new file mode 100644 index 000000000..e87837ce8 --- /dev/null +++ b/docs/plans/2026-03-23-job-shop-scheduling.md @@ -0,0 +1,295 @@ +# JobShopScheduling Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add the `JobShopScheduling` satisfaction model, CLI creation support, canonical example data, tests, and paper documentation for issue `#510`. + +**Architecture:** Represent a witness as one permutation per machine, encoded with concatenated Lehmer-code segments. `evaluate()` will decode those machine orders, orient the disjunctive graph, reject cyclic orientations, and compute earliest start times by longest-path propagation; the schedule is feasible iff every task completes by the global deadline. This intentionally supersedes the issue body’s unverified “start-time variable” sketch because the issue comments and example statistics (`6! * 6! = 518400` task-orderings) clearly assume machine-order enumeration. + +**Tech Stack:** Rust core model registry, serde/inventory metadata, `problemreductions-cli` create command, example-db exports, Typst paper docs. + +--- + +## Batch 1: add-model Steps 1-5.5 + +### Task 1: Write the model tests first + +**Files:** +- Create: `src/unit_tests/models/misc/job_shop_scheduling.rs` +- Reference: `src/unit_tests/models/misc/flow_shop_scheduling.rs` + +**Step 1: Write the failing tests** + +Add targeted tests that define the intended semantics before any production code exists: +- `test_job_shop_scheduling_creation_and_dims` + - Construct `JobShopScheduling::new(2, vec![vec![(0, 3), (1, 4)], vec![(1, 2), (0, 3), (1, 2)]], 20)` + - Assert `num_processors() == 2`, `num_jobs() == 2`, `num_tasks() == 5` + - Assert `dims() == vec![2, 1, 3, 2, 1]` for machine-0 tasks `[j0.t0, j1.t1]` and machine-1 tasks `[j0.t1, j1.t0, j1.t2]` +- `test_job_shop_scheduling_evaluate_issue_example` + - Use the corrected issue instance with 5 jobs / 12 tasks / deadline `20` + - Assert the machine-order config `[0, 0, 0, 0, 0, 0, 1, 3, 0, 1, 1, 0]` evaluates to `true` +- `test_job_shop_scheduling_rejects_machine_overlap_or_cycle` + - Use a small 2-machine instance whose chosen machine orders force a precedence cycle, and assert `evaluate()` returns `false` +- `test_job_shop_scheduling_invalid_config_and_serialization` + - Reject wrong-length or out-of-range Lehmer digits + - Round-trip through `serde_json` +- `test_job_shop_scheduling_solver_small_instance` + - Use a tiny 2-job / 2-machine instance where brute force can find a satisfying witness + +**Step 2: Run the tests to verify they fail** + +Run: `cargo test job_shop_scheduling --lib` + +Expected: FAIL because `JobShopScheduling` and its test linkage do not exist yet. + +**Step 3: Commit the red test file once it exists and fails cleanly** + +Run: +```bash +git add src/unit_tests/models/misc/job_shop_scheduling.rs +git commit -m "test: add red tests for JobShopScheduling" +``` + +### Task 2: Implement the core model and schedule evaluator + +**Files:** +- Create: `src/models/misc/job_shop_scheduling.rs` +- Modify: `src/models/misc/mod.rs` +- Modify: `src/models/mod.rs` +- Modify: `src/lib.rs` + +**Step 1: Add the model scaffold** + +Implement `JobShopScheduling` with: +- `num_processors: usize` +- `jobs: Vec>` +- `deadline: u64` + +Register `ProblemSchemaEntry` with constructor-facing fields: +- `num_processors: usize` +- `jobs: Vec>` +- `deadline: u64` + +Constructor invariants: +- every processor index is `< num_processors` +- consecutive tasks within a job use different processors (Garey-Johnson formulation) +- `num_processors > 0` when the instance contains tasks + +Add getters: +- `num_processors()` +- `jobs()` +- `deadline()` +- `num_jobs()` +- `num_tasks()` + +**Step 2: Implement the permutation-based witness encoding** + +Add helpers that: +- flatten tasks into stable task ids in `(job_index, task_index)` order +- group task ids by machine in ascending task-id order +- decode one Lehmer-code segment per machine into an ordered list of task ids +- concatenate segment dimensions in `dims()` as `[k, k-1, ..., 1]` for each machine with `k` assigned tasks + +Use `Problem::Metric = bool`, `variant() = crate::variant_params![]`, and `impl SatisfactionProblem for JobShopScheduling {}`. + +**Step 3: Implement `evaluate()` by disjunctive-graph orientation** + +`evaluate(config)` should: +- reject invalid config length or out-of-range Lehmer digits +- decode per-machine task orders +- build directed edges: + - job-precedence edge `u -> v` with weight `len(u)` + - machine-order edge `u -> v` with weight `len(u)` +- run topological sort on the oriented graph; if cyclic, return `false` +- compute earliest start times by longest-path DP over the DAG +- return `true` iff every task finishes by `deadline` + +Expose a small helper such as `schedule_from_config(&self, config) -> Option>` if it keeps the paper/example tests readable. + +**Step 4: Register the model and complexity metadata** + +Update exports in: +- `src/models/misc/mod.rs` +- `src/models/mod.rs` +- `src/lib.rs` + +Add: +```rust +crate::declare_variants! { + default sat JobShopScheduling => "factorial(num_tasks)", +} +``` + +Use `factorial(num_tasks)` rather than the issue body’s `factorial(num_jobs)` because the chosen witness representation enumerates machine task orders, and `factorial(num_jobs)` undercounts jobs with more than one operation on the same machine. + +**Step 5: Run the focused tests** + +Run: `cargo test job_shop_scheduling --lib` + +Expected: PASS for the new model tests. + +**Step 6: Commit the core model** + +Run: +```bash +git add src/models/misc/job_shop_scheduling.rs src/models/misc/mod.rs src/models/mod.rs src/lib.rs +git commit -m "feat: add JobShopScheduling model" +``` + +### Task 3: Register example-db coverage and trait consistency + +**Files:** +- Modify: `src/models/misc/mod.rs` +- Modify: `src/unit_tests/trait_consistency.rs` +- Modify: `src/example_db/model_builders.rs` (only if needed by the existing pattern) + +**Step 1: Add the canonical example spec in the model file** + +Inside `src/models/misc/job_shop_scheduling.rs`, add: +- `canonical_model_example_specs()` +- the corrected issue example with deadline `20` +- canonical satisfying config `[0, 0, 0, 0, 0, 0, 1, 3, 0, 1, 1, 0]` + +Then wire the model into the `misc::canonical_model_example_specs()` chain. + +**Step 2: Extend smoke coverage** + +Add a `check_problem_trait(...)` entry for `JobShopScheduling` in `src/unit_tests/trait_consistency.rs`. + +If example-db tests require any explicit expectations for the new example, add them in the existing example-db test module instead of inventing a new harness. + +**Step 3: Run focused tests** + +Run: +```bash +cargo test trait_consistency +cargo test example_db --features example-db +``` + +Expected: PASS, with the new model visible to registry/example-db consumers. + +**Step 4: Commit the registration changes** + +Run: +```bash +git add src/models/misc/mod.rs src/unit_tests/trait_consistency.rs src/example_db/model_builders.rs +git commit -m "test: register JobShopScheduling example coverage" +``` + +### Task 4: Add CLI discovery and `pred create` support + +**Files:** +- Modify: `problemreductions-cli/src/problem_name.rs` +- Modify: `problemreductions-cli/src/cli.rs` +- Modify: `problemreductions-cli/src/commands/create.rs` + +**Step 1: Add the CLI input shape** + +Add a new `CreateArgs` flag: +- `--job-tasks` + +Format: +- semicolon-separated jobs +- comma-separated operations per job +- each operation encoded as `processor:length` +- example: `--job-tasks "0:3,1:4;1:2,0:3,1:2;0:4,1:3"` + +Update: +- `all_data_flags_empty()` +- the “Flags by problem type” help table +- usage/help text strings mentioning `JobShopScheduling` + +**Step 2: Add name resolution and constructor parsing** + +In `problem_name.rs`, add the lowercase canonical mapping for `jobshopscheduling`. + +In `create.rs`: +- add an example string for `JobShopScheduling` +- parse `--job-tasks`, `--deadline`, and optional `--num-processors` +- infer `num_processors` as `1 + max(processor index)` when the flag is omitted +- validate every parsed processor index against the resolved processor count +- serialize `JobShopScheduling::new(...)` + +**Step 3: Add CLI tests first, then implementation wiring** + +Use the existing `create.rs` unit-test section to add: +- one success case that round-trips the issue-style example +- one failure case for malformed `processor:length` +- one failure case for a missing `--job-tasks` + +Run: +```bash +cargo test -p problemreductions-cli create::tests::job_shop +``` + +Expected: RED before the parser arm exists, then GREEN after the arm/help updates are added. + +**Step 4: Commit the CLI support** + +Run: +```bash +git add problemreductions-cli/src/problem_name.rs problemreductions-cli/src/cli.rs problemreductions-cli/src/commands/create.rs +git commit -m "feat: add JobShopScheduling CLI support" +``` + +## Batch 2: add-model Step 6 + +### Task 5: Document the model in the paper and align the worked example + +**Files:** +- Modify: `docs/paper/reductions.typ` +- Reference: `docs/paper/reductions.typ` `FlowShopScheduling` entry + +**Step 1: Add the display name and `problem-def` entry** + +Register: +- `"JobShopScheduling": [Job-Shop Scheduling]` + +Then add a `#problem-def("JobShopScheduling")[...][...]` entry that: +- defines jobs as ordered task sequences with processor assignments and lengths +- explicitly calls out the Garey-Johnson “consecutive tasks use different processors” formulation +- explains the permutation-per-machine witness representation used in the implementation + +**Step 2: Reuse the corrected canonical example** + +In the paper body: +- load the example with `load-model-example("JobShopScheduling")` +- decode the machine-order config into earliest start times using the same reasoning as the Rust helper +- present the corrected 5-job / 2-machine / deadline-20 instance +- include a simple Gantt-style figure and a short explanation that the derived makespan is `19` + +**Step 3: Add a paper-example test** + +Back in `src/unit_tests/models/misc/job_shop_scheduling.rs`, add `test_job_shop_scheduling_paper_example` that: +- constructs the same canonical example +- evaluates the canonical config +- optionally checks the derived start-time vector or makespan `19` + +**Step 4: Run verification** + +Run: +```bash +cargo test job_shop_scheduling --lib +make paper +``` + +Expected: PASS, with the paper example and canonical example in sync. + +**Step 5: Commit the paper/docs batch** + +Run: +```bash +git add docs/paper/reductions.typ src/unit_tests/models/misc/job_shop_scheduling.rs +git commit -m "docs: add JobShopScheduling paper entry" +``` + +## Final Verification + +After all tasks are green, run the full issue gate: + +```bash +make test +make clippy +``` + +If the paper/example/export workflow updates tracked generated files that belong with the feature, stage them explicitly and keep ignored `docs/src/reductions/` outputs out of the commit. From 3a931406ecf040a7b1edba94259da01804a4fc90 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 23 Mar 2026 16:58:11 +0800 Subject: [PATCH 2/8] Implement #510: [Model] JobShopScheduling --- docs/paper/reductions.typ | 92 ++++++ problemreductions-cli/src/cli.rs | 8 +- problemreductions-cli/src/commands/create.rs | 189 ++++++++++++- problemreductions-cli/src/problem_name.rs | 3 + src/lib.rs | 15 +- src/models/misc/job_shop_scheduling.rs | 267 ++++++++++++++++++ src/models/misc/mod.rs | 4 + src/models/mod.rs | 6 +- src/unit_tests/example_db.rs | 18 ++ .../models/misc/job_shop_scheduling.rs | 93 ++++++ src/unit_tests/trait_consistency.rs | 4 + 11 files changed, 679 insertions(+), 20 deletions(-) create mode 100644 src/models/misc/job_shop_scheduling.rs create mode 100644 src/unit_tests/models/misc/job_shop_scheduling.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 5b94df3f3..4c4028416 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -145,6 +145,7 @@ "IntegralFlowWithMultipliers": [Integral Flow With Multipliers], "MinMaxMulticenter": [Min-Max Multicenter], "FlowShopScheduling": [Flow Shop Scheduling], + "JobShopScheduling": [Job-Shop Scheduling], "GroupingBySwapping": [Grouping by Swapping], "MinimumCutIntoBoundedSets": [Minimum Cut Into Bounded Sets], "MinimumDummyActivitiesPert": [Minimum Dummy Activities in PERT Networks], @@ -5126,6 +5127,97 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], ] } +#{ + let x = load-model-example("JobShopScheduling") + let D = x.instance.deadline + let blocks = ( + (0, 0, 0, 0, 3), + (0, 1, 1, 3, 6), + (0, 2, 0, 6, 10), + (0, 3, 1, 10, 12), + (0, 4, 0, 12, 14), + (0, 4, 2, 17, 18), + (1, 1, 0, 0, 2), + (1, 3, 0, 2, 7), + (1, 0, 1, 7, 11), + (1, 2, 1, 11, 14), + (1, 4, 1, 14, 17), + (1, 1, 2, 17, 19), + ) + let makespan = 19 + [ + #problem-def("JobShopScheduling")[ + 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$, consecutive-processor constraint $p(t_k[j]) != p(t_(k+1)[j])$, and a deadline $D in ZZ^+$, 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 every job finishes by time $D$. + ][ + Job-Shop Scheduling is the classical disjunctive scheduling problem SS18 in Garey & Johnson; Garey, Johnson, and Sethi proved it strongly NP-complete 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 checking whether those local orders admit global start times. 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.]. + + *Example.* The canonical fixture has two machines, deadline $D = #D$, and five jobs + $ + J_1 = ((M_1, 3), (M_2, 4)), + J_2 = ((M_2, 2), (M_1, 3), (M_2, 2)), + J_3 = ((M_1, 4), (M_2, 3)), + J_4 = ((M_2, 5), (M_1, 2)), + J_5 = ((M_1, 2), (M_2, 3), (M_1, 1)). + $ + 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 last completion time is $#makespan <= #D$, so the verifier returns YES. + + #pred-commands( + "pred create --example " + problem-spec(x) + " -o job-shop-scheduling.json", + "pred solve job-shop-scheduling.json --solver brute-force", + "pred evaluate job-shop-scheduling.json --config " + x.optimal_config.map(str).join(","), + ) + + #figure( + canvas(length: 1cm, { + import draw: * + let colors = (rgb("#4e79a7"), rgb("#e15759"), rgb("#76b7b2"), rgb("#f28e2b"), rgb("#59a14f")) + let scale = 0.38 + let row-h = 0.6 + let gap = 0.15 + + for mi in range(2) { + let y = -mi * (row-h + gap) + content((-0.8, y), text(8pt, "M" + str(mi + 1))) + } + + for block in blocks { + let (mi, ji, ti, s, e) = block + let x0 = s * scale + let x1 = e * scale + let y = -mi * (row-h + gap) + rect( + (x0, y - row-h / 2), + (x1, y + row-h / 2), + fill: colors.at(ji).transparentize(30%), + stroke: 0.4pt + colors.at(ji), + ) + content(((x0 + x1) / 2, y), text(6pt, "j" + str(ji + 1) + "." + str(ti + 1))) + } + + let y-axis = -(2 - 1) * (row-h + gap) - row-h / 2 - 0.2 + line((0, y-axis), (makespan * scale, y-axis), stroke: 0.4pt) + for t in range(calc.ceil(makespan / 5) + 1).map(i => calc.min(i * 5, makespan)) { + let x = t * scale + line((x, y-axis), (x, y-axis - 0.1), stroke: 0.4pt) + content((x, y-axis - 0.25), text(6pt, str(t))) + } + if calc.rem(makespan, 5) != 0 { + let x = makespan * scale + line((x, y-axis), (x, y-axis - 0.1), stroke: 0.4pt) + content((x, y-axis - 0.25), text(6pt, str(makespan))) + } + content((makespan * scale / 2, y-axis - 0.5), text(7pt)[$t$]) + + let dl-x = D * scale + line((dl-x, row-h / 2 + 0.1), (dl-x, y-axis), stroke: (paint: red, thickness: 0.8pt, dash: "dashed")) + content((dl-x, row-h / 2 + 0.25), text(6pt, fill: red)[$D = #D$]) + }), + caption: [Job-shop schedule induced by the canonical machine-order witness. The final completion time is #makespan, which stays to the left of the deadline marker $D = #D$.], + ) + ] + ] +} + #problem-def("StaffScheduling")[ 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. ][ diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 54dcf8ee0..ef8c34405 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -298,6 +298,7 @@ Flags by problem type: PartiallyOrderedKnapsack --sizes, --values, --capacity, --precedences QAP --matrix (cost), --distance-matrix StrongConnectivityAugmentation --arcs, --candidate-arcs, --bound [--num-vertices] + JobShopScheduling --job-tasks, --deadline [--num-processors] FlowShopScheduling --task-lengths, --deadline [--num-processors] StaffScheduling --schedules, --requirements, --num-workers, --k TimetableDesign --num-periods, --num-craftsmen, --num-tasks, --craftsman-avail, --task-avail, --requirements @@ -646,10 +647,13 @@ pub struct CreateArgs { /// Task lengths for FlowShopScheduling (semicolon-separated rows: "3,4,2;2,3,5;4,1,3") #[arg(long)] pub task_lengths: Option, - /// Deadline for FlowShopScheduling, MultiprocessorScheduling, or ResourceConstrainedScheduling + /// Job tasks for JobShopScheduling (semicolon-separated jobs, comma-separated processor:length tasks, e.g., "0:3,1:4;1:2,0:3,1:2") + #[arg(long)] + pub job_tasks: Option, + /// Deadline for FlowShopScheduling, JobShopScheduling, MultiprocessorScheduling, or ResourceConstrainedScheduling #[arg(long)] pub deadline: Option, - /// Number of processors/machines for FlowShopScheduling, MultiprocessorScheduling, ResourceConstrainedScheduling, or SchedulingWithIndividualDeadlines + /// Number of processors/machines for FlowShopScheduling, JobShopScheduling, MultiprocessorScheduling, ResourceConstrainedScheduling, or SchedulingWithIndividualDeadlines #[arg(long)] pub num_processors: Option, /// Binary schedule patterns for StaffScheduling (semicolon-separated rows, e.g., "1,1,0;0,1,1") diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 9c65f9262..f05ab659e 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -22,14 +22,14 @@ use problemreductions::models::graph::{ use problemreductions::models::misc::{ AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CapacityAssignment, CbqRelation, ConjunctiveBooleanQuery, ConsistencyOfDatabaseFrequencyTables, EnsembleComputation, - ExpectedRetrievalCost, FlowShopScheduling, FrequencyTable, GroupingBySwapping, KnownValue, - LongestCommonSubsequence, MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, - PartiallyOrderedKnapsack, QueryArg, RectilinearPictureCompression, - ResourceConstrainedScheduling, SchedulingWithIndividualDeadlines, - SequencingToMinimizeMaximumCumulativeCost, SequencingToMinimizeWeightedCompletionTime, - SequencingToMinimizeWeightedTardiness, SequencingWithReleaseTimesAndDeadlines, - SequencingWithinIntervals, ShortestCommonSupersequence, StringToStringCorrection, SubsetSum, - SumOfSquaresPartition, TimetableDesign, + ExpectedRetrievalCost, FlowShopScheduling, FrequencyTable, GroupingBySwapping, + JobShopScheduling, KnownValue, LongestCommonSubsequence, MinimumTardinessSequencing, + MultiprocessorScheduling, PaintShop, PartiallyOrderedKnapsack, QueryArg, + RectilinearPictureCompression, ResourceConstrainedScheduling, + SchedulingWithIndividualDeadlines, SequencingToMinimizeMaximumCumulativeCost, + SequencingToMinimizeWeightedCompletionTime, SequencingToMinimizeWeightedTardiness, + SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, ShortestCommonSupersequence, + StringToStringCorrection, SubsetSum, SumOfSquaresPartition, TimetableDesign, }; use problemreductions::models::BiconnectivityAugmentation; use problemreductions::prelude::*; @@ -150,6 +150,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.resource_bounds.is_none() && args.resource_requirements.is_none() && args.task_lengths.is_none() + && args.job_tasks.is_none() && args.deadline.is_none() && args.num_processors.is_none() && args.schedules.is_none() @@ -449,6 +450,51 @@ fn parse_precedence_pairs(raw: Option<&str>) -> Result> { .unwrap_or_else(|| Ok(vec![])) } +fn parse_job_shop_jobs(raw: &str) -> Result>> { + let raw = raw.trim(); + if raw.is_empty() { + return Ok(vec![]); + } + + raw.split(';') + .enumerate() + .map(|(job_index, job_str)| { + let job_str = job_str.trim(); + anyhow::ensure!( + !job_str.is_empty(), + "Invalid --job-tasks value: empty job at position {}", + job_index + ); + + job_str + .split(',') + .map(|task_str| { + let task_str = task_str.trim(); + let (processor, length) = task_str.split_once(':').ok_or_else(|| { + anyhow::anyhow!( + "Invalid --job-tasks operation '{}': expected 'processor:length'", + task_str + ) + })?; + let processor = processor.trim().parse::().map_err(|_| { + anyhow::anyhow!( + "Invalid --job-tasks operation '{}': processor must be a nonnegative integer", + task_str + ) + })?; + let length = length.trim().parse::().map_err(|_| { + anyhow::anyhow!( + "Invalid --job-tasks operation '{}': length must be a nonnegative integer", + task_str + ) + })?; + Ok((processor, length)) + }) + .collect() + }) + .collect() +} + fn validate_precedence_pairs(precedences: &[(usize, usize)], num_tasks: usize) -> Result<()> { for &(pred, succ) in precedences { anyhow::ensure!( @@ -620,6 +666,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "--capacities 1,2,3 --cost-matrix \"1,3,6;2,4,7;1,2,5\" --delay-matrix \"8,4,1;7,3,1;6,3,1\" --cost-budget 10 --delay-budget 12" } "MultiprocessorScheduling" => "--lengths 4,5,3,2,6 --num-processors 2 --deadline 10", + "JobShopScheduling" => { + "--job-tasks \"0:3,1:4;1:2,0:3,1:2;0:4,1:3;1:5,0:2;0:2,1:3,0:1\" --deadline 20 --num-processors 2" + } "MinimumMultiwayCut" => "--graph 0-1,1-2,2-3 --terminals 0,2 --edge-weights 1,1,1", "ExpectedRetrievalCost" => EXPECTED_RETRIEVAL_COST_EXAMPLE_ARGS, "SequencingWithinIntervals" => "--release-times 0,0,5 --deadlines 11,11,6 --lengths 3,1,1", @@ -735,9 +784,11 @@ fn help_flag_name(canonical: &str, field_name: &str) -> String { ("BoundedComponentSpanningForest", "max_components") => return "k".to_string(), ("BoundedComponentSpanningForest", "max_weight") => return "bound".to_string(), ("FlowShopScheduling", "num_processors") + | ("JobShopScheduling", "num_processors") | ("SchedulingWithIndividualDeadlines", "num_processors") => { return "num-processors/--m".to_string(); } + ("JobShopScheduling", "jobs") => return "job-tasks".to_string(), ("LengthBoundedDisjointPaths", "max_length") => return "bound".to_string(), ("RectilinearPictureCompression", "bound") => return "bound".to_string(), ("PrimeAttributeName", "num_attributes") => return "universe".to_string(), @@ -3560,6 +3611,51 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // JobShopScheduling + "JobShopScheduling" => { + let usage = "Usage: pred create JobShopScheduling --job-tasks \"0:3,1:4;1:2,0:3,1:2;0:4,1:3\" --deadline 20 --num-processors 2"; + let job_tasks = args.job_tasks.as_deref().ok_or_else(|| { + anyhow::anyhow!("JobShopScheduling requires --job-tasks and --deadline\n\n{usage}") + })?; + let deadline = args.deadline.ok_or_else(|| { + anyhow::anyhow!("JobShopScheduling requires --deadline\n\n{usage}") + })?; + let jobs = parse_job_shop_jobs(job_tasks)?; + let inferred_processors = jobs + .iter() + .flat_map(|job| job.iter().map(|(processor, _)| *processor)) + .max() + .map(|processor| processor + 1); + let num_processors = resolve_processor_count_flags( + "JobShopScheduling", + usage, + args.num_processors, + args.m, + )? + .or(inferred_processors) + .ok_or_else(|| { + anyhow::anyhow!( + "Cannot infer num_processors from empty job list; use --num-processors" + ) + })?; + anyhow::ensure!( + num_processors > 0, + "JobShopScheduling requires --num-processors > 0\n\n{usage}" + ); + for (job_index, job) in jobs.iter().enumerate() { + for (task_index, &(processor, _)) in job.iter().enumerate() { + anyhow::ensure!( + processor < num_processors, + "job {job_index} task {task_index} uses processor {processor}, but num_processors = {num_processors}" + ); + } + } + ( + ser(JobShopScheduling::new(num_processors, jobs, deadline))?, + resolved_variant.clone(), + ) + } + // StaffScheduling "StaffScheduling" => { let usage = "Usage: pred create StaffScheduling --schedules \"1,1,1,1,1,0,0;0,1,1,1,1,1,0;0,0,1,1,1,1,1;1,0,0,1,1,1,1;1,1,0,0,1,1,1\" --requirements 2,2,2,3,3,2,1 --num-workers 4 --k 5"; @@ -7311,6 +7407,7 @@ mod tests { deadlines: None, precedence_pairs: None, task_lengths: None, + job_tasks: None, resource_bounds: None, resource_requirements: None, deadline: None, @@ -7379,6 +7476,13 @@ mod tests { assert!(!all_data_flags_empty(&args)); } + #[test] + fn test_all_data_flags_empty_treats_job_tasks_as_input() { + let mut args = empty_args(); + args.job_tasks = Some("0:1,1:1;1:1,0:1".to_string()); + assert!(!all_data_flags_empty(&args)); + } + #[test] fn test_parse_potential_edges() { let mut args = empty_args(); @@ -7713,6 +7817,75 @@ mod tests { assert!(err.contains("ExpectedRetrievalCost requires --latency-bound")); } + #[test] + fn test_create_job_shop_scheduling_json() { + use crate::dispatch::ProblemJsonOutput; + use problemreductions::models::misc::JobShopScheduling; + + let mut args = empty_args(); + args.problem = Some("JobShopScheduling".to_string()); + args.job_tasks = Some("0:3,1:4;1:2,0:3,1:2;0:4,1:3;1:5,0:2;0:2,1:3,0:1".to_string()); + args.deadline = Some(20); + + let output_path = + std::env::temp_dir().join(format!("job-shop-scheduling-{}.json", std::process::id())); + let out = OutputConfig { + output: Some(output_path.clone()), + quiet: true, + json: false, + auto_json: false, + }; + + create(&args, &out).unwrap(); + + let json = std::fs::read_to_string(&output_path).unwrap(); + let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap(); + assert_eq!(created.problem_type, "JobShopScheduling"); + assert!(created.variant.is_empty()); + + let problem: JobShopScheduling = serde_json::from_value(created.data).unwrap(); + assert_eq!(problem.num_processors(), 2); + assert_eq!(problem.num_jobs(), 5); + assert!(problem.evaluate(&[0, 0, 0, 0, 0, 0, 1, 3, 0, 1, 1, 0])); + + let _ = std::fs::remove_file(output_path); + } + + #[test] + fn test_create_job_shop_scheduling_requires_job_tasks() { + let mut args = empty_args(); + args.problem = Some("JobShopScheduling".to_string()); + args.deadline = Some(20); + + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + + let err = create(&args, &out).unwrap_err().to_string(); + assert!(err.contains("JobShopScheduling requires --job-tasks")); + } + + #[test] + fn test_create_job_shop_scheduling_rejects_malformed_operation() { + let mut args = empty_args(); + args.problem = Some("JobShopScheduling".to_string()); + args.job_tasks = Some("0-3,1:4".to_string()); + args.deadline = Some(20); + + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + + let err = create(&args, &out).unwrap_err().to_string(); + assert!(err.contains("expected 'processor:length'")); + } + #[test] fn test_create_rooted_tree_storage_assignment_json() { let mut args = empty_args(); diff --git a/problemreductions-cli/src/problem_name.rs b/problemreductions-cli/src/problem_name.rs index 94817e754..ea0d8a204 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -20,6 +20,9 @@ pub fn resolve_alias(input: &str) -> String { if input.eq_ignore_ascii_case("GroupingBySwapping") { return "GroupingBySwapping".to_string(); } + if input.eq_ignore_ascii_case("JobShopScheduling") { + return "JobShopScheduling".to_string(); + } if let Some(pt) = problemreductions::registry::find_problem_type_by_alias(input) { return pt.canonical_name.to_string(); } diff --git a/src/lib.rs b/src/lib.rs index 56b6902ea..2e21f45c0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -72,13 +72,14 @@ pub mod prelude { AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CapacityAssignment, CbqRelation, ConjunctiveBooleanQuery, ConjunctiveQueryFoldability, ConsistencyOfDatabaseFrequencyTables, EnsembleComputation, ExpectedRetrievalCost, Factoring, FlowShopScheduling, - GroupingBySwapping, Knapsack, LongestCommonSubsequence, MinimumTardinessSequencing, - MultiprocessorScheduling, PaintShop, Partition, QueryArg, RectilinearPictureCompression, - ResourceConstrainedScheduling, SchedulingWithIndividualDeadlines, - SequencingToMinimizeMaximumCumulativeCost, SequencingToMinimizeWeightedCompletionTime, - SequencingToMinimizeWeightedTardiness, SequencingWithReleaseTimesAndDeadlines, - SequencingWithinIntervals, ShortestCommonSupersequence, StackerCrane, StaffScheduling, - StringToStringCorrection, SubsetSum, SumOfSquaresPartition, Term, TimetableDesign, + GroupingBySwapping, JobShopScheduling, Knapsack, LongestCommonSubsequence, + MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, Partition, QueryArg, + RectilinearPictureCompression, ResourceConstrainedScheduling, + SchedulingWithIndividualDeadlines, SequencingToMinimizeMaximumCumulativeCost, + SequencingToMinimizeWeightedCompletionTime, SequencingToMinimizeWeightedTardiness, + SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, + ShortestCommonSupersequence, StackerCrane, StaffScheduling, StringToStringCorrection, + SubsetSum, SumOfSquaresPartition, Term, TimetableDesign, }; pub use crate::models::set::{ ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking, diff --git a/src/models/misc/job_shop_scheduling.rs b/src/models/misc/job_shop_scheduling.rs new file mode 100644 index 000000000..e8acf8310 --- /dev/null +++ b/src/models/misc/job_shop_scheduling.rs @@ -0,0 +1,267 @@ +//! Job Shop Scheduling problem implementation. +//! +//! Given `m` processors and a set of jobs, each job consisting of an ordered +//! sequence of processor-length tasks, determine whether the tasks can be +//! scheduled to finish by a global deadline while respecting both within-job +//! precedence and single-processor capacity constraints. + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; +use std::collections::VecDeque; + +inventory::submit! { + ProblemSchemaEntry { + name: "JobShopScheduling", + display_name: "Job-Shop Scheduling", + aliases: &[], + dimensions: &[], + module_path: module_path!(), + description: "Determine whether a job-shop schedule meets a global deadline", + fields: &[ + FieldInfo { name: "num_processors", type_name: "usize", description: "Number of processors m" }, + FieldInfo { name: "jobs", type_name: "Vec>", description: "jobs[j][k] = (processor, length) for the k-th task of job j" }, + FieldInfo { name: "deadline", type_name: "u64", description: "Global deadline D" }, + ], + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct JobShopScheduling { + num_processors: usize, + jobs: Vec>, + deadline: u64, +} + +struct FlattenedTasks { + job_task_ids: Vec>, + machine_task_ids: Vec>, + lengths: Vec, +} + +impl JobShopScheduling { + pub fn new(num_processors: usize, jobs: Vec>, deadline: u64) -> Self { + let num_tasks: usize = jobs.iter().map(Vec::len).sum(); + if num_tasks > 0 { + assert!( + num_processors > 0, + "num_processors must be positive when tasks are present" + ); + } + + for (job_index, job) in jobs.iter().enumerate() { + for (task_index, &(processor, _length)) in job.iter().enumerate() { + assert!( + processor < num_processors, + "job {job_index} task {task_index} uses processor {processor}, but num_processors = {num_processors}" + ); + } + + for (task_index, pair) in job.windows(2).enumerate() { + assert_ne!( + pair[0].0, + pair[1].0, + "job {job_index} tasks {task_index} and {} must use different processors", + task_index + 1 + ); + } + } + + Self { + num_processors, + jobs, + deadline, + } + } + + pub fn num_processors(&self) -> usize { + self.num_processors + } + + pub fn jobs(&self) -> &[Vec<(usize, u64)>] { + &self.jobs + } + + pub fn deadline(&self) -> u64 { + self.deadline + } + + pub fn num_jobs(&self) -> usize { + self.jobs.len() + } + + pub fn num_tasks(&self) -> usize { + self.jobs.iter().map(Vec::len).sum() + } + + fn flatten_tasks(&self) -> FlattenedTasks { + let mut job_task_ids = Vec::with_capacity(self.jobs.len()); + let mut machine_task_ids = vec![Vec::new(); self.num_processors]; + let mut lengths = Vec::with_capacity(self.num_tasks()); + let mut task_id = 0usize; + + for job in &self.jobs { + let mut ids = Vec::with_capacity(job.len()); + for &(processor, length) in job { + ids.push(task_id); + machine_task_ids[processor].push(task_id); + lengths.push(length); + task_id += 1; + } + job_task_ids.push(ids); + } + + FlattenedTasks { + job_task_ids, + machine_task_ids, + lengths, + } + } + + fn decode_machine_orders( + &self, + config: &[usize], + flattened: &FlattenedTasks, + ) -> Option>> { + if config.len() != flattened.lengths.len() { + return None; + } + + let mut offset = 0usize; + let mut orders = Vec::with_capacity(flattened.machine_task_ids.len()); + + for machine_tasks in &flattened.machine_task_ids { + let next_offset = offset + machine_tasks.len(); + let segment = &config[offset..next_offset]; + offset = next_offset; + + let mut available = machine_tasks.clone(); + let mut order = Vec::with_capacity(machine_tasks.len()); + for &digit in segment { + if digit >= available.len() { + return None; + } + order.push(available.remove(digit)); + } + orders.push(order); + } + + Some(orders) + } + + fn schedule_from_config(&self, config: &[usize]) -> Option> { + let flattened = self.flatten_tasks(); + let machine_orders = self.decode_machine_orders(config, &flattened)?; + let num_tasks = flattened.lengths.len(); + + if num_tasks == 0 { + return Some(Vec::new()); + } + + let mut adjacency = vec![Vec::::new(); num_tasks]; + let mut indegree = vec![0usize; num_tasks]; + + for job_ids in &flattened.job_task_ids { + for pair in job_ids.windows(2) { + adjacency[pair[0]].push(pair[1]); + indegree[pair[1]] += 1; + } + } + + for machine_order in &machine_orders { + for pair in machine_order.windows(2) { + adjacency[pair[0]].push(pair[1]); + indegree[pair[1]] += 1; + } + } + + let mut queue = VecDeque::new(); + for (task_id, °ree) in indegree.iter().enumerate() { + if degree == 0 { + queue.push_back(task_id); + } + } + + let mut start_times = vec![0u64; num_tasks]; + let mut processed = 0usize; + + while let Some(task_id) = queue.pop_front() { + processed += 1; + let finish = start_times[task_id].checked_add(flattened.lengths[task_id])?; + + for &next_task in &adjacency[task_id] { + start_times[next_task] = start_times[next_task].max(finish); + indegree[next_task] -= 1; + if indegree[next_task] == 0 { + queue.push_back(next_task); + } + } + } + + if processed != num_tasks { + return None; + } + + for (task_id, &start) in start_times.iter().enumerate() { + let finish = start.checked_add(flattened.lengths[task_id])?; + if finish > self.deadline { + return None; + } + } + + Some(start_times) + } +} + +impl Problem for JobShopScheduling { + const NAME: &'static str = "JobShopScheduling"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + self.flatten_tasks() + .machine_task_ids + .into_iter() + .flat_map(|machine_tasks| (0..machine_tasks.len()).rev().map(|i| i + 1)) + .collect() + } + + fn evaluate(&self, config: &[usize]) -> bool { + self.schedule_from_config(config).is_some() + } +} + +impl SatisfactionProblem for JobShopScheduling {} + +crate::declare_variants! { + default sat JobShopScheduling => "factorial(num_tasks)", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "job_shop_scheduling", + instance: Box::new(JobShopScheduling::new( + 2, + vec![ + vec![(0, 3), (1, 4)], + vec![(1, 2), (0, 3), (1, 2)], + vec![(0, 4), (1, 3)], + vec![(1, 5), (0, 2)], + vec![(0, 2), (1, 3), (0, 1)], + ], + 20, + )), + // Machine 0 order [0,3,5,8,9,11] => [0,0,0,0,0,0] + // Machine 1 order [2,7,1,6,10,4] => [1,3,0,1,1,0] + optimal_config: vec![0, 0, 0, 0, 0, 0, 1, 3, 0, 1, 1, 0], + optimal_value: serde_json::json!(true), + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/job_shop_scheduling.rs"] +mod tests; diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index 2d07dc9bd..db6ac7319 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -11,6 +11,7 @@ //! - [`Factoring`]: Integer factorization //! - [`FlowShopScheduling`]: Flow Shop Scheduling (meet deadline on m processors) //! - [`GroupingBySwapping`]: Group equal symbols into contiguous blocks by adjacent swaps +//! - [`JobShopScheduling`]: Meet a deadline with per-job processor routes //! - [`Knapsack`]: 0-1 Knapsack (maximize value subject to weight capacity) //! - [`MultiprocessorScheduling`]: Schedule tasks on processors to meet a deadline //! - [`LongestCommonSubsequence`]: Longest Common Subsequence @@ -46,6 +47,7 @@ pub(crate) mod expected_retrieval_cost; pub(crate) mod factoring; mod flow_shop_scheduling; mod grouping_by_swapping; +mod job_shop_scheduling; mod knapsack; mod longest_common_subsequence; mod minimum_tardiness_sequencing; @@ -84,6 +86,7 @@ pub use expected_retrieval_cost::ExpectedRetrievalCost; pub use factoring::Factoring; pub use flow_shop_scheduling::FlowShopScheduling; pub use grouping_by_swapping::GroupingBySwapping; +pub use job_shop_scheduling::JobShopScheduling; pub use knapsack::Knapsack; pub use longest_common_subsequence::LongestCommonSubsequence; pub use minimum_tardiness_sequencing::MinimumTardinessSequencing; @@ -141,6 +144,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec JobShopScheduling { + JobShopScheduling::new( + 2, + vec![ + vec![(0, 3), (1, 4)], + vec![(1, 2), (0, 3), (1, 2)], + vec![(0, 4), (1, 3)], + vec![(1, 5), (0, 2)], + vec![(0, 2), (1, 3), (0, 1)], + ], + 20, + ) +} + +fn small_two_job_instance() -> JobShopScheduling { + JobShopScheduling::new(2, vec![vec![(0, 1), (1, 1)], vec![(1, 1), (0, 1)]], 2) +} + +#[test] +fn test_job_shop_scheduling_creation_and_dims() { + let problem = issue_example(); + assert_eq!(problem.num_processors(), 2); + assert_eq!(problem.num_jobs(), 5); + assert_eq!(problem.num_tasks(), 12); + assert_eq!(problem.dims(), vec![6, 5, 4, 3, 2, 1, 6, 5, 4, 3, 2, 1]); +} + +#[test] +fn test_job_shop_scheduling_evaluate_issue_example() { + let problem = issue_example(); + let config = vec![0, 0, 0, 0, 0, 0, 1, 3, 0, 1, 1, 0]; + assert!(problem.evaluate(&config)); +} + +#[test] +fn test_job_shop_scheduling_paper_example_schedule() { + let problem = issue_example(); + let config = vec![0, 0, 0, 0, 0, 0, 1, 3, 0, 1, 1, 0]; + let start_times = problem.schedule_from_config(&config).unwrap(); + assert_eq!(start_times, vec![0, 7, 0, 3, 17, 6, 11, 2, 10, 12, 14, 17]); + + let makespan = start_times + .iter() + .zip( + problem + .jobs() + .iter() + .flat_map(|job| job.iter().map(|(_, length)| *length)), + ) + .map(|(&start, length)| start + length) + .max() + .unwrap(); + assert_eq!(makespan, 19); +} + +#[test] +fn test_job_shop_scheduling_rejects_cyclic_machine_orders() { + let problem = small_two_job_instance(); + let config = vec![1, 0, 0, 0]; + assert!(!problem.evaluate(&config)); +} + +#[test] +fn test_job_shop_scheduling_invalid_config_and_serialization() { + let problem = small_two_job_instance(); + assert!(!problem.evaluate(&[2, 0, 0, 0])); + assert!(!problem.evaluate(&[0, 0, 0])); + + let json = serde_json::to_value(&problem).unwrap(); + let restored: JobShopScheduling = serde_json::from_value(json).unwrap(); + assert_eq!(restored.num_processors(), problem.num_processors()); + assert_eq!(restored.jobs(), problem.jobs()); + assert_eq!(restored.deadline(), problem.deadline()); +} + +#[test] +fn test_job_shop_scheduling_problem_name_and_variant() { + assert_eq!(::NAME, "JobShopScheduling"); + assert!(::variant().is_empty()); +} + +#[test] +fn test_job_shop_scheduling_brute_force_solver_small_instance() { + let problem = small_two_job_instance(); + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!(solution.is_some()); + assert!(problem.evaluate(&solution.unwrap())); +} diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index f45862e05..9e40f051d 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -201,6 +201,10 @@ fn test_all_problems_implement_trait_correctly() { &FlowShopScheduling::new(2, vec![vec![1, 2], vec![3, 4]], 10), "FlowShopScheduling", ); + check_problem_trait( + &JobShopScheduling::new(2, vec![vec![(0, 1), (1, 1)], vec![(1, 1), (0, 1)]], 2), + "JobShopScheduling", + ); check_problem_trait( &SequencingToMinimizeWeightedTardiness::new(vec![3, 4, 2], vec![2, 3, 1], vec![5, 8, 4], 4), "SequencingToMinimizeWeightedTardiness", From c6331749f2c94a1d5cb46170ed48015cfe640945 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 23 Mar 2026 16:58:19 +0800 Subject: [PATCH 3/8] chore: remove plan file after implementation --- docs/plans/2026-03-23-job-shop-scheduling.md | 295 ------------------- 1 file changed, 295 deletions(-) delete mode 100644 docs/plans/2026-03-23-job-shop-scheduling.md diff --git a/docs/plans/2026-03-23-job-shop-scheduling.md b/docs/plans/2026-03-23-job-shop-scheduling.md deleted file mode 100644 index e87837ce8..000000000 --- a/docs/plans/2026-03-23-job-shop-scheduling.md +++ /dev/null @@ -1,295 +0,0 @@ -# JobShopScheduling Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Add the `JobShopScheduling` satisfaction model, CLI creation support, canonical example data, tests, and paper documentation for issue `#510`. - -**Architecture:** Represent a witness as one permutation per machine, encoded with concatenated Lehmer-code segments. `evaluate()` will decode those machine orders, orient the disjunctive graph, reject cyclic orientations, and compute earliest start times by longest-path propagation; the schedule is feasible iff every task completes by the global deadline. This intentionally supersedes the issue body’s unverified “start-time variable” sketch because the issue comments and example statistics (`6! * 6! = 518400` task-orderings) clearly assume machine-order enumeration. - -**Tech Stack:** Rust core model registry, serde/inventory metadata, `problemreductions-cli` create command, example-db exports, Typst paper docs. - ---- - -## Batch 1: add-model Steps 1-5.5 - -### Task 1: Write the model tests first - -**Files:** -- Create: `src/unit_tests/models/misc/job_shop_scheduling.rs` -- Reference: `src/unit_tests/models/misc/flow_shop_scheduling.rs` - -**Step 1: Write the failing tests** - -Add targeted tests that define the intended semantics before any production code exists: -- `test_job_shop_scheduling_creation_and_dims` - - Construct `JobShopScheduling::new(2, vec![vec![(0, 3), (1, 4)], vec![(1, 2), (0, 3), (1, 2)]], 20)` - - Assert `num_processors() == 2`, `num_jobs() == 2`, `num_tasks() == 5` - - Assert `dims() == vec![2, 1, 3, 2, 1]` for machine-0 tasks `[j0.t0, j1.t1]` and machine-1 tasks `[j0.t1, j1.t0, j1.t2]` -- `test_job_shop_scheduling_evaluate_issue_example` - - Use the corrected issue instance with 5 jobs / 12 tasks / deadline `20` - - Assert the machine-order config `[0, 0, 0, 0, 0, 0, 1, 3, 0, 1, 1, 0]` evaluates to `true` -- `test_job_shop_scheduling_rejects_machine_overlap_or_cycle` - - Use a small 2-machine instance whose chosen machine orders force a precedence cycle, and assert `evaluate()` returns `false` -- `test_job_shop_scheduling_invalid_config_and_serialization` - - Reject wrong-length or out-of-range Lehmer digits - - Round-trip through `serde_json` -- `test_job_shop_scheduling_solver_small_instance` - - Use a tiny 2-job / 2-machine instance where brute force can find a satisfying witness - -**Step 2: Run the tests to verify they fail** - -Run: `cargo test job_shop_scheduling --lib` - -Expected: FAIL because `JobShopScheduling` and its test linkage do not exist yet. - -**Step 3: Commit the red test file once it exists and fails cleanly** - -Run: -```bash -git add src/unit_tests/models/misc/job_shop_scheduling.rs -git commit -m "test: add red tests for JobShopScheduling" -``` - -### Task 2: Implement the core model and schedule evaluator - -**Files:** -- Create: `src/models/misc/job_shop_scheduling.rs` -- Modify: `src/models/misc/mod.rs` -- Modify: `src/models/mod.rs` -- Modify: `src/lib.rs` - -**Step 1: Add the model scaffold** - -Implement `JobShopScheduling` with: -- `num_processors: usize` -- `jobs: Vec>` -- `deadline: u64` - -Register `ProblemSchemaEntry` with constructor-facing fields: -- `num_processors: usize` -- `jobs: Vec>` -- `deadline: u64` - -Constructor invariants: -- every processor index is `< num_processors` -- consecutive tasks within a job use different processors (Garey-Johnson formulation) -- `num_processors > 0` when the instance contains tasks - -Add getters: -- `num_processors()` -- `jobs()` -- `deadline()` -- `num_jobs()` -- `num_tasks()` - -**Step 2: Implement the permutation-based witness encoding** - -Add helpers that: -- flatten tasks into stable task ids in `(job_index, task_index)` order -- group task ids by machine in ascending task-id order -- decode one Lehmer-code segment per machine into an ordered list of task ids -- concatenate segment dimensions in `dims()` as `[k, k-1, ..., 1]` for each machine with `k` assigned tasks - -Use `Problem::Metric = bool`, `variant() = crate::variant_params![]`, and `impl SatisfactionProblem for JobShopScheduling {}`. - -**Step 3: Implement `evaluate()` by disjunctive-graph orientation** - -`evaluate(config)` should: -- reject invalid config length or out-of-range Lehmer digits -- decode per-machine task orders -- build directed edges: - - job-precedence edge `u -> v` with weight `len(u)` - - machine-order edge `u -> v` with weight `len(u)` -- run topological sort on the oriented graph; if cyclic, return `false` -- compute earliest start times by longest-path DP over the DAG -- return `true` iff every task finishes by `deadline` - -Expose a small helper such as `schedule_from_config(&self, config) -> Option>` if it keeps the paper/example tests readable. - -**Step 4: Register the model and complexity metadata** - -Update exports in: -- `src/models/misc/mod.rs` -- `src/models/mod.rs` -- `src/lib.rs` - -Add: -```rust -crate::declare_variants! { - default sat JobShopScheduling => "factorial(num_tasks)", -} -``` - -Use `factorial(num_tasks)` rather than the issue body’s `factorial(num_jobs)` because the chosen witness representation enumerates machine task orders, and `factorial(num_jobs)` undercounts jobs with more than one operation on the same machine. - -**Step 5: Run the focused tests** - -Run: `cargo test job_shop_scheduling --lib` - -Expected: PASS for the new model tests. - -**Step 6: Commit the core model** - -Run: -```bash -git add src/models/misc/job_shop_scheduling.rs src/models/misc/mod.rs src/models/mod.rs src/lib.rs -git commit -m "feat: add JobShopScheduling model" -``` - -### Task 3: Register example-db coverage and trait consistency - -**Files:** -- Modify: `src/models/misc/mod.rs` -- Modify: `src/unit_tests/trait_consistency.rs` -- Modify: `src/example_db/model_builders.rs` (only if needed by the existing pattern) - -**Step 1: Add the canonical example spec in the model file** - -Inside `src/models/misc/job_shop_scheduling.rs`, add: -- `canonical_model_example_specs()` -- the corrected issue example with deadline `20` -- canonical satisfying config `[0, 0, 0, 0, 0, 0, 1, 3, 0, 1, 1, 0]` - -Then wire the model into the `misc::canonical_model_example_specs()` chain. - -**Step 2: Extend smoke coverage** - -Add a `check_problem_trait(...)` entry for `JobShopScheduling` in `src/unit_tests/trait_consistency.rs`. - -If example-db tests require any explicit expectations for the new example, add them in the existing example-db test module instead of inventing a new harness. - -**Step 3: Run focused tests** - -Run: -```bash -cargo test trait_consistency -cargo test example_db --features example-db -``` - -Expected: PASS, with the new model visible to registry/example-db consumers. - -**Step 4: Commit the registration changes** - -Run: -```bash -git add src/models/misc/mod.rs src/unit_tests/trait_consistency.rs src/example_db/model_builders.rs -git commit -m "test: register JobShopScheduling example coverage" -``` - -### Task 4: Add CLI discovery and `pred create` support - -**Files:** -- Modify: `problemreductions-cli/src/problem_name.rs` -- Modify: `problemreductions-cli/src/cli.rs` -- Modify: `problemreductions-cli/src/commands/create.rs` - -**Step 1: Add the CLI input shape** - -Add a new `CreateArgs` flag: -- `--job-tasks` - -Format: -- semicolon-separated jobs -- comma-separated operations per job -- each operation encoded as `processor:length` -- example: `--job-tasks "0:3,1:4;1:2,0:3,1:2;0:4,1:3"` - -Update: -- `all_data_flags_empty()` -- the “Flags by problem type” help table -- usage/help text strings mentioning `JobShopScheduling` - -**Step 2: Add name resolution and constructor parsing** - -In `problem_name.rs`, add the lowercase canonical mapping for `jobshopscheduling`. - -In `create.rs`: -- add an example string for `JobShopScheduling` -- parse `--job-tasks`, `--deadline`, and optional `--num-processors` -- infer `num_processors` as `1 + max(processor index)` when the flag is omitted -- validate every parsed processor index against the resolved processor count -- serialize `JobShopScheduling::new(...)` - -**Step 3: Add CLI tests first, then implementation wiring** - -Use the existing `create.rs` unit-test section to add: -- one success case that round-trips the issue-style example -- one failure case for malformed `processor:length` -- one failure case for a missing `--job-tasks` - -Run: -```bash -cargo test -p problemreductions-cli create::tests::job_shop -``` - -Expected: RED before the parser arm exists, then GREEN after the arm/help updates are added. - -**Step 4: Commit the CLI support** - -Run: -```bash -git add problemreductions-cli/src/problem_name.rs problemreductions-cli/src/cli.rs problemreductions-cli/src/commands/create.rs -git commit -m "feat: add JobShopScheduling CLI support" -``` - -## Batch 2: add-model Step 6 - -### Task 5: Document the model in the paper and align the worked example - -**Files:** -- Modify: `docs/paper/reductions.typ` -- Reference: `docs/paper/reductions.typ` `FlowShopScheduling` entry - -**Step 1: Add the display name and `problem-def` entry** - -Register: -- `"JobShopScheduling": [Job-Shop Scheduling]` - -Then add a `#problem-def("JobShopScheduling")[...][...]` entry that: -- defines jobs as ordered task sequences with processor assignments and lengths -- explicitly calls out the Garey-Johnson “consecutive tasks use different processors” formulation -- explains the permutation-per-machine witness representation used in the implementation - -**Step 2: Reuse the corrected canonical example** - -In the paper body: -- load the example with `load-model-example("JobShopScheduling")` -- decode the machine-order config into earliest start times using the same reasoning as the Rust helper -- present the corrected 5-job / 2-machine / deadline-20 instance -- include a simple Gantt-style figure and a short explanation that the derived makespan is `19` - -**Step 3: Add a paper-example test** - -Back in `src/unit_tests/models/misc/job_shop_scheduling.rs`, add `test_job_shop_scheduling_paper_example` that: -- constructs the same canonical example -- evaluates the canonical config -- optionally checks the derived start-time vector or makespan `19` - -**Step 4: Run verification** - -Run: -```bash -cargo test job_shop_scheduling --lib -make paper -``` - -Expected: PASS, with the paper example and canonical example in sync. - -**Step 5: Commit the paper/docs batch** - -Run: -```bash -git add docs/paper/reductions.typ src/unit_tests/models/misc/job_shop_scheduling.rs -git commit -m "docs: add JobShopScheduling paper entry" -``` - -## Final Verification - -After all tasks are green, run the full issue gate: - -```bash -make test -make clippy -``` - -If the paper/example/export workflow updates tracked generated files that belong with the feature, stage them explicitly and keep ignored `docs/src/reductions/` outputs out of the commit. From f89f53a45fa6af264336d3f41d086f7248dc92e7 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Fri, 27 Mar 2026 14:32:33 +0800 Subject: [PATCH 4/8] =?UTF-8?q?fix:=20adapt=20JobShopScheduling=20to=20Val?= =?UTF-8?q?ue=20trait=20(Metric=E2=86=92Value=20rename)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- problemreductions-cli/src/cli.rs | 2 +- problemreductions-cli/src/commands/create.rs | 19 ------------------- src/models/misc/job_shop_scheduling.rs | 13 ++++++------- .../models/misc/job_shop_scheduling.rs | 15 ++++++++------- 4 files changed, 15 insertions(+), 34 deletions(-) diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index e979ab236..dea6e65a4 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -887,7 +887,7 @@ mod tests { )); assert!( help.contains( - "Number of processors/machines for FlowShopScheduling, MultiprocessorScheduling, ResourceConstrainedScheduling, or SchedulingWithIndividualDeadlines" + "Number of processors/machines for FlowShopScheduling, JobShopScheduling, MultiprocessorScheduling, ResourceConstrainedScheduling, or SchedulingWithIndividualDeadlines" ), "create help should describe --num-processors for both scheduling models" ); diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 022c60e19..b9a733e89 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -7696,25 +7696,6 @@ mod tests { let _ = std::fs::remove_file(output_path); } - #[test] - fn test_create_expected_retrieval_cost_requires_latency_bound() { - let mut args = empty_args(); - args.problem = Some("ExpectedRetrievalCost".to_string()); - args.probabilities = Some("0.2,0.15,0.15,0.2,0.1,0.2".to_string()); - args.num_sectors = Some(3); - args.latency_bound = None; - - let out = OutputConfig { - output: None, - quiet: true, - json: false, - auto_json: false, - }; - - let err = create(&args, &out).unwrap_err().to_string(); - assert!(err.contains("ExpectedRetrievalCost requires --latency-bound")); - } - #[test] fn test_create_job_shop_scheduling_json() { use crate::dispatch::ProblemJsonOutput; diff --git a/src/models/misc/job_shop_scheduling.rs b/src/models/misc/job_shop_scheduling.rs index e8acf8310..c6d3e9b22 100644 --- a/src/models/misc/job_shop_scheduling.rs +++ b/src/models/misc/job_shop_scheduling.rs @@ -6,7 +6,8 @@ //! precedence and single-processor capacity constraints. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; +use crate::types::Or; use serde::{Deserialize, Serialize}; use std::collections::VecDeque; @@ -215,7 +216,7 @@ impl JobShopScheduling { impl Problem for JobShopScheduling { const NAME: &'static str = "JobShopScheduling"; - type Metric = bool; + type Value = Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -229,15 +230,13 @@ impl Problem for JobShopScheduling { .collect() } - fn evaluate(&self, config: &[usize]) -> bool { - self.schedule_from_config(config).is_some() + fn evaluate(&self, config: &[usize]) -> Or { + Or(self.schedule_from_config(config).is_some()) } } -impl SatisfactionProblem for JobShopScheduling {} - crate::declare_variants! { - default sat JobShopScheduling => "factorial(num_tasks)", + default JobShopScheduling => "factorial(num_tasks)", } #[cfg(feature = "example-db")] diff --git a/src/unit_tests/models/misc/job_shop_scheduling.rs b/src/unit_tests/models/misc/job_shop_scheduling.rs index 99182a060..56e303a70 100644 --- a/src/unit_tests/models/misc/job_shop_scheduling.rs +++ b/src/unit_tests/models/misc/job_shop_scheduling.rs @@ -1,6 +1,7 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; +use crate::types::Or; fn issue_example() -> JobShopScheduling { JobShopScheduling::new( @@ -33,7 +34,7 @@ fn test_job_shop_scheduling_creation_and_dims() { fn test_job_shop_scheduling_evaluate_issue_example() { let problem = issue_example(); let config = vec![0, 0, 0, 0, 0, 0, 1, 3, 0, 1, 1, 0]; - assert!(problem.evaluate(&config)); + assert_eq!(problem.evaluate(&config), Or(true)); } #[test] @@ -61,14 +62,14 @@ fn test_job_shop_scheduling_paper_example_schedule() { fn test_job_shop_scheduling_rejects_cyclic_machine_orders() { let problem = small_two_job_instance(); let config = vec![1, 0, 0, 0]; - assert!(!problem.evaluate(&config)); + assert_eq!(problem.evaluate(&config), Or(false)); } #[test] fn test_job_shop_scheduling_invalid_config_and_serialization() { let problem = small_two_job_instance(); - assert!(!problem.evaluate(&[2, 0, 0, 0])); - assert!(!problem.evaluate(&[0, 0, 0])); + assert_eq!(problem.evaluate(&[2, 0, 0, 0]), Or(false)); + assert_eq!(problem.evaluate(&[0, 0, 0]), Or(false)); let json = serde_json::to_value(&problem).unwrap(); let restored: JobShopScheduling = serde_json::from_value(json).unwrap(); @@ -87,7 +88,7 @@ fn test_job_shop_scheduling_problem_name_and_variant() { fn test_job_shop_scheduling_brute_force_solver_small_instance() { let problem = small_two_job_instance(); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); - assert!(problem.evaluate(&solution.unwrap())); + assert_eq!(problem.evaluate(&solution.unwrap()), Or(true)); } From c050cde95e76b420ce3c382ec1b7b6a02de14ee7 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Fri, 27 Mar 2026 16:19:22 +0800 Subject: [PATCH 5/8] refactor: convert JobShopScheduling from satisfaction to optimization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change Value from Or to Min (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) --- docs/paper/reductions.typ | 15 ++---- problemreductions-cli/src/cli.rs | 4 +- problemreductions-cli/src/commands/create.rs | 46 +++++++++++++---- src/models/misc/job_shop_scheduling.rs | 51 ++++++++++--------- .../models/misc/job_shop_scheduling.rs | 23 ++++----- 5 files changed, 79 insertions(+), 60 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 0027beb2c..6f29860cf 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -5125,7 +5125,6 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], #{ let x = load-model-example("JobShopScheduling") - let D = x.instance.deadline let blocks = ( (0, 0, 0, 0, 3), (0, 1, 1, 3, 6), @@ -5143,11 +5142,11 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let makespan = 19 [ #problem-def("JobShopScheduling")[ - 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$, consecutive-processor constraint $p(t_k[j]) != p(t_(k+1)[j])$, and a deadline $D in ZZ^+$, 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 every job finishes by time $D$. + 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. ][ - Job-Shop Scheduling is the classical disjunctive scheduling problem SS18 in Garey & Johnson; Garey, Johnson, and Sethi proved it strongly NP-complete 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 checking whether those local orders admit global start times. 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.]. + 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.]. - *Example.* The canonical fixture has two machines, deadline $D = #D$, and five jobs + *Example.* The canonical fixture has two machines and five jobs $ J_1 = ((M_1, 3), (M_2, 4)), J_2 = ((M_2, 2), (M_1, 3), (M_2, 2)), @@ -5155,7 +5154,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], J_4 = ((M_2, 5), (M_1, 2)), J_5 = ((M_1, 2), (M_2, 3), (M_1, 1)). $ - 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 last completion time is $#makespan <= #D$, so the verifier returns YES. + 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$. #pred-commands( "pred create --example " + problem-spec(x) + " -o job-shop-scheduling.json", @@ -5203,12 +5202,8 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], content((x, y-axis - 0.25), text(6pt, str(makespan))) } content((makespan * scale / 2, y-axis - 0.5), text(7pt)[$t$]) - - let dl-x = D * scale - line((dl-x, row-h / 2 + 0.1), (dl-x, y-axis), stroke: (paint: red, thickness: 0.8pt, dash: "dashed")) - content((dl-x, row-h / 2 + 0.25), text(6pt, fill: red)[$D = #D$]) }), - caption: [Job-shop schedule induced by the canonical machine-order witness. The final completion time is #makespan, which stays to the left of the deadline marker $D = #D$.], + caption: [Job-shop schedule induced by the canonical machine-order witness. The optimal makespan is #makespan.], ) ] ] diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index dea6e65a4..fe0b7bd35 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -298,7 +298,7 @@ Flags by problem type: PartiallyOrderedKnapsack --sizes, --values, --capacity, --precedences QAP --matrix (cost), --distance-matrix StrongConnectivityAugmentation --arcs, --candidate-arcs, --bound [--num-vertices] - JobShopScheduling --job-tasks, --deadline [--num-processors] + JobShopScheduling --job-tasks [--num-processors] FlowShopScheduling --task-lengths, --deadline [--num-processors] StaffScheduling --schedules, --requirements, --num-workers, --k TimetableDesign --num-periods, --num-craftsmen, --num-tasks, --craftsman-avail, --task-avail, --requirements @@ -647,7 +647,7 @@ pub struct CreateArgs { /// Job tasks for JobShopScheduling (semicolon-separated jobs, comma-separated processor:length tasks, e.g., "0:3,1:4;1:2,0:3,1:2") #[arg(long)] pub job_tasks: Option, - /// Deadline for FlowShopScheduling, JobShopScheduling, MultiprocessorScheduling, or ResourceConstrainedScheduling + /// Deadline for FlowShopScheduling, MultiprocessorScheduling, or ResourceConstrainedScheduling #[arg(long)] pub deadline: Option, /// Number of processors/machines for FlowShopScheduling, JobShopScheduling, MultiprocessorScheduling, ResourceConstrainedScheduling, or SchedulingWithIndividualDeadlines diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index b9a733e89..d295670c6 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -665,7 +665,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { } "MultiprocessorScheduling" => "--lengths 4,5,3,2,6 --num-processors 2 --deadline 10", "JobShopScheduling" => { - "--job-tasks \"0:3,1:4;1:2,0:3,1:2;0:4,1:3;1:5,0:2;0:2,1:3,0:1\" --deadline 20 --num-processors 2" + "--job-tasks \"0:3,1:4;1:2,0:3,1:2;0:4,1:3;1:5,0:2;0:2,1:3,0:1\" --num-processors 2" } "MinimumMultiwayCut" => "--graph 0-1,1-2,2-3 --terminals 0,2 --edge-weights 1,1,1", "ExpectedRetrievalCost" => EXPECTED_RETRIEVAL_COST_EXAMPLE_ARGS, @@ -3503,12 +3503,9 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { // JobShopScheduling "JobShopScheduling" => { - let usage = "Usage: pred create JobShopScheduling --job-tasks \"0:3,1:4;1:2,0:3,1:2;0:4,1:3\" --deadline 20 --num-processors 2"; + let usage = "Usage: pred create JobShopScheduling --job-tasks \"0:3,1:4;1:2,0:3,1:2;0:4,1:3\" --num-processors 2"; let job_tasks = args.job_tasks.as_deref().ok_or_else(|| { - anyhow::anyhow!("JobShopScheduling requires --job-tasks and --deadline\n\n{usage}") - })?; - let deadline = args.deadline.ok_or_else(|| { - anyhow::anyhow!("JobShopScheduling requires --deadline\n\n{usage}") + anyhow::anyhow!("JobShopScheduling requires --job-tasks\n\n{usage}") })?; let jobs = parse_job_shop_jobs(job_tasks)?; let inferred_processors = jobs @@ -3539,9 +3536,16 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { "job {job_index} task {task_index} uses processor {processor}, but num_processors = {num_processors}" ); } + for (task_index, pair) in job.windows(2).enumerate() { + anyhow::ensure!( + pair[0].0 != pair[1].0, + "job {job_index} tasks {task_index} and {} must use different processors\n\n{usage}", + task_index + 1 + ); + } } ( - ser(JobShopScheduling::new(num_processors, jobs, deadline))?, + ser(JobShopScheduling::new(num_processors, jobs))?, resolved_variant.clone(), ) } @@ -7700,11 +7704,12 @@ mod tests { fn test_create_job_shop_scheduling_json() { use crate::dispatch::ProblemJsonOutput; use problemreductions::models::misc::JobShopScheduling; + use problemreductions::traits::Problem; + use problemreductions::types::Min; let mut args = empty_args(); args.problem = Some("JobShopScheduling".to_string()); args.job_tasks = Some("0:3,1:4;1:2,0:3,1:2;0:4,1:3;1:5,0:2;0:2,1:3,0:1".to_string()); - args.deadline = Some(20); let output_path = std::env::temp_dir().join(format!("job-shop-scheduling-{}.json", std::process::id())); @@ -7725,7 +7730,10 @@ mod tests { let problem: JobShopScheduling = serde_json::from_value(created.data).unwrap(); assert_eq!(problem.num_processors(), 2); assert_eq!(problem.num_jobs(), 5); - assert!(problem.evaluate(&[0, 0, 0, 0, 0, 0, 1, 3, 0, 1, 1, 0])); + assert_eq!( + problem.evaluate(&[0, 0, 0, 0, 0, 0, 1, 3, 0, 1, 1, 0]), + Min(Some(19)) + ); let _ = std::fs::remove_file(output_path); } @@ -7734,7 +7742,7 @@ mod tests { fn test_create_job_shop_scheduling_requires_job_tasks() { let mut args = empty_args(); args.problem = Some("JobShopScheduling".to_string()); - args.deadline = Some(20); + args.num_processors = Some(2); let out = OutputConfig { output: None, @@ -7752,7 +7760,6 @@ mod tests { let mut args = empty_args(); args.problem = Some("JobShopScheduling".to_string()); args.job_tasks = Some("0-3,1:4".to_string()); - args.deadline = Some(20); let out = OutputConfig { output: None, @@ -7765,6 +7772,23 @@ mod tests { assert!(err.contains("expected 'processor:length'")); } + #[test] + fn test_create_job_shop_scheduling_rejects_consecutive_same_processor() { + let mut args = empty_args(); + args.problem = Some("JobShopScheduling".to_string()); + args.job_tasks = Some("0:1,0:1".to_string()); + + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + + let err = create(&args, &out).unwrap_err().to_string(); + assert!(err.contains("must use different processors")); + } + #[test] fn test_create_rooted_tree_storage_assignment_json() { let mut args = empty_args(); diff --git a/src/models/misc/job_shop_scheduling.rs b/src/models/misc/job_shop_scheduling.rs index c6d3e9b22..27670f7cc 100644 --- a/src/models/misc/job_shop_scheduling.rs +++ b/src/models/misc/job_shop_scheduling.rs @@ -1,13 +1,13 @@ //! Job Shop Scheduling problem implementation. //! //! Given `m` processors and a set of jobs, each job consisting of an ordered -//! sequence of processor-length tasks, determine whether the tasks can be -//! scheduled to finish by a global deadline while respecting both within-job +//! sequence of processor-length tasks, find a schedule that minimizes the +//! makespan (completion time of the last task) while respecting both within-job //! precedence and single-processor capacity constraints. use crate::registry::{FieldInfo, ProblemSchemaEntry}; use crate::traits::Problem; -use crate::types::Or; +use crate::types::Min; use serde::{Deserialize, Serialize}; use std::collections::VecDeque; @@ -18,11 +18,10 @@ inventory::submit! { aliases: &[], dimensions: &[], module_path: module_path!(), - description: "Determine whether a job-shop schedule meets a global deadline", + description: "Minimize the makespan of a job-shop schedule", fields: &[ FieldInfo { name: "num_processors", type_name: "usize", description: "Number of processors m" }, FieldInfo { name: "jobs", type_name: "Vec>", description: "jobs[j][k] = (processor, length) for the k-th task of job j" }, - FieldInfo { name: "deadline", type_name: "u64", description: "Global deadline D" }, ], } } @@ -31,7 +30,6 @@ inventory::submit! { pub struct JobShopScheduling { num_processors: usize, jobs: Vec>, - deadline: u64, } struct FlattenedTasks { @@ -41,7 +39,7 @@ struct FlattenedTasks { } impl JobShopScheduling { - pub fn new(num_processors: usize, jobs: Vec>, deadline: u64) -> Self { + pub fn new(num_processors: usize, jobs: Vec>) -> Self { let num_tasks: usize = jobs.iter().map(Vec::len).sum(); if num_tasks > 0 { assert!( @@ -71,7 +69,6 @@ impl JobShopScheduling { Self { num_processors, jobs, - deadline, } } @@ -83,10 +80,6 @@ impl JobShopScheduling { &self.jobs } - pub fn deadline(&self) -> u64 { - self.deadline - } - pub fn num_jobs(&self) -> usize { self.jobs.len() } @@ -150,7 +143,9 @@ impl JobShopScheduling { Some(orders) } - fn schedule_from_config(&self, config: &[usize]) -> Option> { + /// Compute start times from a Lehmer-code config. Returns `None` if the + /// config is invalid or induces a cycle in the precedence DAG. + pub fn schedule_from_config(&self, config: &[usize]) -> Option> { let flattened = self.flatten_tasks(); let machine_orders = self.decode_machine_orders(config, &flattened)?; let num_tasks = flattened.lengths.len(); @@ -203,20 +198,24 @@ impl JobShopScheduling { return None; } - for (task_id, &start) in start_times.iter().enumerate() { - let finish = start.checked_add(flattened.lengths[task_id])?; - if finish > self.deadline { - return None; - } - } - Some(start_times) } + + /// Compute the makespan (completion time of last task) from start times. + fn makespan(&self, start_times: &[u64]) -> u64 { + let flattened = self.flatten_tasks(); + start_times + .iter() + .enumerate() + .map(|(i, &s)| s + flattened.lengths[i]) + .max() + .unwrap_or(0) + } } impl Problem for JobShopScheduling { const NAME: &'static str = "JobShopScheduling"; - type Value = Or; + type Value = Min; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -230,8 +229,11 @@ impl Problem for JobShopScheduling { .collect() } - fn evaluate(&self, config: &[usize]) -> Or { - Or(self.schedule_from_config(config).is_some()) + fn evaluate(&self, config: &[usize]) -> Min { + match self.schedule_from_config(config) { + Some(start_times) => Min(Some(self.makespan(&start_times))), + None => Min(None), + } } } @@ -252,12 +254,11 @@ pub(crate) fn canonical_model_example_specs() -> Vec [0,0,0,0,0,0] // Machine 1 order [2,7,1,6,10,4] => [1,3,0,1,1,0] optimal_config: vec![0, 0, 0, 0, 0, 0, 1, 3, 0, 1, 1, 0], - optimal_value: serde_json::json!(true), + optimal_value: serde_json::json!(19), }] } diff --git a/src/unit_tests/models/misc/job_shop_scheduling.rs b/src/unit_tests/models/misc/job_shop_scheduling.rs index 56e303a70..4b252f1d5 100644 --- a/src/unit_tests/models/misc/job_shop_scheduling.rs +++ b/src/unit_tests/models/misc/job_shop_scheduling.rs @@ -1,7 +1,7 @@ use super::*; -use crate::solvers::BruteForce; +use crate::solvers::{BruteForce, Solver}; use crate::traits::Problem; -use crate::types::Or; +use crate::types::Min; fn issue_example() -> JobShopScheduling { JobShopScheduling::new( @@ -13,12 +13,11 @@ fn issue_example() -> JobShopScheduling { vec![(1, 5), (0, 2)], vec![(0, 2), (1, 3), (0, 1)], ], - 20, ) } fn small_two_job_instance() -> JobShopScheduling { - JobShopScheduling::new(2, vec![vec![(0, 1), (1, 1)], vec![(1, 1), (0, 1)]], 2) + JobShopScheduling::new(2, vec![vec![(0, 1), (1, 1)], vec![(1, 1), (0, 1)]]) } #[test] @@ -34,7 +33,7 @@ fn test_job_shop_scheduling_creation_and_dims() { fn test_job_shop_scheduling_evaluate_issue_example() { let problem = issue_example(); let config = vec![0, 0, 0, 0, 0, 0, 1, 3, 0, 1, 1, 0]; - assert_eq!(problem.evaluate(&config), Or(true)); + assert_eq!(problem.evaluate(&config), Min(Some(19))); } #[test] @@ -62,20 +61,19 @@ fn test_job_shop_scheduling_paper_example_schedule() { fn test_job_shop_scheduling_rejects_cyclic_machine_orders() { let problem = small_two_job_instance(); let config = vec![1, 0, 0, 0]; - assert_eq!(problem.evaluate(&config), Or(false)); + assert_eq!(problem.evaluate(&config), Min(None)); } #[test] fn test_job_shop_scheduling_invalid_config_and_serialization() { let problem = small_two_job_instance(); - assert_eq!(problem.evaluate(&[2, 0, 0, 0]), Or(false)); - assert_eq!(problem.evaluate(&[0, 0, 0]), Or(false)); + assert_eq!(problem.evaluate(&[2, 0, 0, 0]), Min(None)); + assert_eq!(problem.evaluate(&[0, 0, 0]), Min(None)); let json = serde_json::to_value(&problem).unwrap(); let restored: JobShopScheduling = serde_json::from_value(json).unwrap(); assert_eq!(restored.num_processors(), problem.num_processors()); assert_eq!(restored.jobs(), problem.jobs()); - assert_eq!(restored.deadline(), problem.deadline()); } #[test] @@ -88,7 +86,8 @@ fn test_job_shop_scheduling_problem_name_and_variant() { fn test_job_shop_scheduling_brute_force_solver_small_instance() { let problem = small_two_job_instance(); let solver = BruteForce::new(); - let solution = solver.find_witness(&problem); - assert!(solution.is_some()); - assert_eq!(problem.evaluate(&solution.unwrap()), Or(true)); + let value = Solver::solve(&solver, &problem); + assert_eq!(value, Min(Some(2))); + let witness = solver.find_witness(&problem).unwrap(); + assert_eq!(problem.evaluate(&witness), Min(Some(2))); } From ab0c47ca7f321228e827b33cf09de7be7f319224 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Fri, 27 Mar 2026 17:35:46 +0800 Subject: [PATCH 6/8] 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) --- problemreductions-cli/src/problem_name.rs | 3 -- src/models/misc/job_shop_scheduling.rs | 35 +++++++++++++---------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/problemreductions-cli/src/problem_name.rs b/problemreductions-cli/src/problem_name.rs index 27cb27371..c449dac47 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -20,9 +20,6 @@ pub fn resolve_alias(input: &str) -> String { if input.eq_ignore_ascii_case("GroupingBySwapping") { return "GroupingBySwapping".to_string(); } - if input.eq_ignore_ascii_case("JobShopScheduling") { - return "JobShopScheduling".to_string(); - } if let Some(pt) = problemreductions::registry::find_problem_type_by_alias(input) { return pt.canonical_name.to_string(); } diff --git a/src/models/misc/job_shop_scheduling.rs b/src/models/misc/job_shop_scheduling.rs index 27670f7cc..dc18569ff 100644 --- a/src/models/misc/job_shop_scheduling.rs +++ b/src/models/misc/job_shop_scheduling.rs @@ -146,8 +146,15 @@ impl JobShopScheduling { /// Compute start times from a Lehmer-code config. Returns `None` if the /// config is invalid or induces a cycle in the precedence DAG. pub fn schedule_from_config(&self, config: &[usize]) -> Option> { - let flattened = self.flatten_tasks(); - let machine_orders = self.decode_machine_orders(config, &flattened)?; + self.schedule_from_config_inner(config, &self.flatten_tasks()) + } + + fn schedule_from_config_inner( + &self, + config: &[usize], + flattened: &FlattenedTasks, + ) -> Option> { + let machine_orders = self.decode_machine_orders(config, flattened)?; let num_tasks = flattened.lengths.len(); if num_tasks == 0 { @@ -200,17 +207,6 @@ impl JobShopScheduling { Some(start_times) } - - /// Compute the makespan (completion time of last task) from start times. - fn makespan(&self, start_times: &[u64]) -> u64 { - let flattened = self.flatten_tasks(); - start_times - .iter() - .enumerate() - .map(|(i, &s)| s + flattened.lengths[i]) - .max() - .unwrap_or(0) - } } impl Problem for JobShopScheduling { @@ -230,8 +226,17 @@ impl Problem for JobShopScheduling { } fn evaluate(&self, config: &[usize]) -> Min { - match self.schedule_from_config(config) { - Some(start_times) => Min(Some(self.makespan(&start_times))), + let flattened = self.flatten_tasks(); + match self.schedule_from_config_inner(config, &flattened) { + Some(start_times) => { + let makespan = start_times + .iter() + .enumerate() + .map(|(i, &s)| s + flattened.lengths[i]) + .max() + .unwrap_or(0); + Min(Some(makespan)) + } None => Min(None), } } From 6a9e95396abeecdaccb692b027c736e9c2abaffe Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Fri, 27 Mar 2026 17:48:16 +0800 Subject: [PATCH 7/8] 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) --- src/models/misc/flow_shop_scheduling.rs | 19 +++---------- src/models/misc/job_shop_scheduling.rs | 19 ++++--------- .../misc/minimum_tardiness_sequencing.rs | 18 +++---------- src/models/misc/mod.rs | 27 ++++++++++++++++++- ...ing_to_minimize_maximum_cumulative_cost.rs | 18 ++----------- ...ng_to_minimize_weighted_completion_time.rs | 18 ++----------- ...quencing_to_minimize_weighted_tardiness.rs | 18 ++----------- ...encing_with_release_times_and_deadlines.rs | 18 +++---------- 8 files changed, 46 insertions(+), 109 deletions(-) diff --git a/src/models/misc/flow_shop_scheduling.rs b/src/models/misc/flow_shop_scheduling.rs index 02f55fb87..d29937b8a 100644 --- a/src/models/misc/flow_shop_scheduling.rs +++ b/src/models/misc/flow_shop_scheduling.rs @@ -167,27 +167,14 @@ impl Problem for FlowShopScheduling { } fn dims(&self) -> Vec { - let n = self.num_jobs(); - (0..n).rev().map(|i| i + 1).collect() + super::lehmer_dims(self.num_jobs()) } fn evaluate(&self, config: &[usize]) -> crate::types::Or { crate::types::Or({ - let n = self.num_jobs(); - if config.len() != n { + let Some(job_order) = super::decode_lehmer(config, self.num_jobs()) else { return crate::types::Or(false); - } - - // Decode Lehmer code into a permutation. - // config[i] must be < n - i (the domain size for position i). - let mut available: Vec = (0..n).collect(); - let mut job_order = Vec::with_capacity(n); - for &c in config.iter() { - if c >= available.len() { - return crate::types::Or(false); - } - job_order.push(available.remove(c)); - } + }; let makespan = self.compute_makespan(&job_order); makespan <= self.deadline diff --git a/src/models/misc/job_shop_scheduling.rs b/src/models/misc/job_shop_scheduling.rs index dc18569ff..be2dbb4bf 100644 --- a/src/models/misc/job_shop_scheduling.rs +++ b/src/models/misc/job_shop_scheduling.rs @@ -125,19 +125,10 @@ impl JobShopScheduling { let mut orders = Vec::with_capacity(flattened.machine_task_ids.len()); for machine_tasks in &flattened.machine_task_ids { - let next_offset = offset + machine_tasks.len(); - let segment = &config[offset..next_offset]; - offset = next_offset; - - let mut available = machine_tasks.clone(); - let mut order = Vec::with_capacity(machine_tasks.len()); - for &digit in segment { - if digit >= available.len() { - return None; - } - order.push(available.remove(digit)); - } - orders.push(order); + let k = machine_tasks.len(); + let perm = super::decode_lehmer(&config[offset..offset + k], k)?; + orders.push(perm.into_iter().map(|i| machine_tasks[i]).collect()); + offset += k; } Some(orders) @@ -221,7 +212,7 @@ impl Problem for JobShopScheduling { self.flatten_tasks() .machine_task_ids .into_iter() - .flat_map(|machine_tasks| (0..machine_tasks.len()).rev().map(|i| i + 1)) + .flat_map(|machine_tasks| super::lehmer_dims(machine_tasks.len())) .collect() } diff --git a/src/models/misc/minimum_tardiness_sequencing.rs b/src/models/misc/minimum_tardiness_sequencing.rs index 17a7fe3d9..eba25bb02 100644 --- a/src/models/misc/minimum_tardiness_sequencing.rs +++ b/src/models/misc/minimum_tardiness_sequencing.rs @@ -132,26 +132,14 @@ impl Problem for MinimumTardinessSequencing { } fn dims(&self) -> Vec { - let n = self.num_tasks; - (0..n).rev().map(|i| i + 1).collect() + super::lehmer_dims(self.num_tasks) } fn evaluate(&self, config: &[usize]) -> Min { let n = self.num_tasks; - if config.len() != n { + let Some(schedule) = super::decode_lehmer(config, n) else { return Min(None); - } - - // Decode Lehmer code into a permutation. - // config[i] must be < n - i (the domain size for position i). - let mut available: Vec = (0..n).collect(); - let mut schedule = Vec::with_capacity(n); - for &c in config.iter() { - if c >= available.len() { - return Min(None); - } - schedule.push(available.remove(c)); - } + }; // schedule[i] = the task scheduled at position i. // We need sigma(task) = position, i.e., the inverse permutation. diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index 4ac440e07..491950d37 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -11,7 +11,7 @@ //! - [`Factoring`]: Integer factorization //! - [`FlowShopScheduling`]: Flow Shop Scheduling (meet deadline on m processors) //! - [`GroupingBySwapping`]: Group equal symbols into contiguous blocks by adjacent swaps -//! - [`JobShopScheduling`]: Meet a deadline with per-job processor routes +//! - [`JobShopScheduling`]: Minimize makespan with per-job processor routes //! - [`Knapsack`]: 0-1 Knapsack (maximize value subject to weight capacity) //! - [`MultiprocessorScheduling`]: Schedule tasks on processors to meet a deadline //! - [`LongestCommonSubsequence`]: Longest Common Subsequence @@ -36,6 +36,31 @@ //! - [`SumOfSquaresPartition`]: Partition integers into K groups minimizing sum of squared group sums pub(crate) mod additional_key; + +/// Decode a Lehmer code into a permutation of `0..n`. +/// +/// Each element of `config` selects from the remaining items: +/// `config[i]` must be `< n - i`. Returns `None` if the config is +/// invalid (wrong length or out-of-range digit). +pub(crate) fn decode_lehmer(config: &[usize], n: usize) -> Option> { + if config.len() != n { + return None; + } + let mut available: Vec = (0..n).collect(); + let mut schedule = Vec::with_capacity(n); + for &digit in config { + if digit >= available.len() { + return None; + } + schedule.push(available.remove(digit)); + } + Some(schedule) +} + +/// Return the Lehmer-code dimension vector `[n, n-1, ..., 1]`. +pub(crate) fn lehmer_dims(n: usize) -> Vec { + (0..n).rev().map(|i| i + 1).collect() +} mod bin_packing; mod boyce_codd_normal_form_violation; mod capacity_assignment; diff --git a/src/models/misc/sequencing_to_minimize_maximum_cumulative_cost.rs b/src/models/misc/sequencing_to_minimize_maximum_cumulative_cost.rs index fa747f180..46627bfcf 100644 --- a/src/models/misc/sequencing_to_minimize_maximum_cumulative_cost.rs +++ b/src/models/misc/sequencing_to_minimize_maximum_cumulative_cost.rs @@ -78,20 +78,7 @@ impl SequencingToMinimizeMaximumCumulativeCost { } fn decode_schedule(&self, config: &[usize]) -> Option> { - let n = self.num_tasks(); - if config.len() != n { - return None; - } - - let mut available: Vec = (0..n).collect(); - let mut schedule = Vec::with_capacity(n); - for &digit in config { - if digit >= available.len() { - return None; - } - schedule.push(available.remove(digit)); - } - Some(schedule) + super::decode_lehmer(config, self.num_tasks()) } } @@ -147,8 +134,7 @@ impl Problem for SequencingToMinimizeMaximumCumulativeCost { } fn dims(&self) -> Vec { - let n = self.num_tasks(); - (0..n).rev().map(|i| i + 1).collect() + super::lehmer_dims(self.num_tasks()) } fn evaluate(&self, config: &[usize]) -> crate::types::Min { diff --git a/src/models/misc/sequencing_to_minimize_weighted_completion_time.rs b/src/models/misc/sequencing_to_minimize_weighted_completion_time.rs index 76cd0962f..62073c83e 100644 --- a/src/models/misc/sequencing_to_minimize_weighted_completion_time.rs +++ b/src/models/misc/sequencing_to_minimize_weighted_completion_time.rs @@ -130,20 +130,7 @@ impl SequencingToMinimizeWeightedCompletionTime { } fn decode_schedule(&self, config: &[usize]) -> Option> { - let n = self.num_tasks(); - if config.len() != n { - return None; - } - - let mut available: Vec = (0..n).collect(); - let mut schedule = Vec::with_capacity(n); - for &digit in config { - if digit >= available.len() { - return None; - } - schedule.push(available.remove(digit)); - } - Some(schedule) + super::decode_lehmer(config, self.num_tasks()) } fn weighted_completion_time(&self, schedule: &[usize]) -> Min { @@ -214,8 +201,7 @@ impl Problem for SequencingToMinimizeWeightedCompletionTime { } fn dims(&self) -> Vec { - let n = self.num_tasks(); - (0..n).rev().map(|i| i + 1).collect() + super::lehmer_dims(self.num_tasks()) } fn evaluate(&self, config: &[usize]) -> Min { diff --git a/src/models/misc/sequencing_to_minimize_weighted_tardiness.rs b/src/models/misc/sequencing_to_minimize_weighted_tardiness.rs index 8d1daa326..946ae2140 100644 --- a/src/models/misc/sequencing_to_minimize_weighted_tardiness.rs +++ b/src/models/misc/sequencing_to_minimize_weighted_tardiness.rs @@ -114,20 +114,7 @@ impl SequencingToMinimizeWeightedTardiness { } fn decode_schedule(&self, config: &[usize]) -> Option> { - let n = self.num_tasks(); - if config.len() != n { - return None; - } - - let mut available: Vec = (0..n).collect(); - let mut schedule = Vec::with_capacity(n); - for &digit in config { - if digit >= available.len() { - return None; - } - schedule.push(available.remove(digit)); - } - Some(schedule) + super::decode_lehmer(config, self.num_tasks()) } fn schedule_weighted_tardiness(&self, schedule: &[usize]) -> Option { @@ -160,8 +147,7 @@ impl Problem for SequencingToMinimizeWeightedTardiness { } fn dims(&self) -> Vec { - let n = self.num_tasks(); - (0..n).rev().map(|i| i + 1).collect() + super::lehmer_dims(self.num_tasks()) } fn evaluate(&self, config: &[usize]) -> crate::types::Or { diff --git a/src/models/misc/sequencing_with_release_times_and_deadlines.rs b/src/models/misc/sequencing_with_release_times_and_deadlines.rs index f81bce5fe..35c7c9607 100644 --- a/src/models/misc/sequencing_with_release_times_and_deadlines.rs +++ b/src/models/misc/sequencing_with_release_times_and_deadlines.rs @@ -113,26 +113,14 @@ impl Problem for SequencingWithReleaseTimesAndDeadlines { } fn dims(&self) -> Vec { - let n = self.num_tasks(); - (0..n).rev().map(|i| i + 1).collect() + super::lehmer_dims(self.num_tasks()) } fn evaluate(&self, config: &[usize]) -> crate::types::Or { crate::types::Or({ - let n = self.num_tasks(); - if config.len() != n { + let Some(schedule) = super::decode_lehmer(config, self.num_tasks()) else { return crate::types::Or(false); - } - - // Decode Lehmer code into a permutation of task indices. - let mut available: Vec = (0..n).collect(); - let mut schedule = Vec::with_capacity(n); - for &c in config.iter() { - if c >= available.len() { - return crate::types::Or(false); - } - schedule.push(available.remove(c)); - } + }; // Schedule tasks left-to-right: each task starts at max(release_time, current_time). let mut current_time: u64 = 0; From adc0139fc0b5835e6185a84c491c7972909f711c Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Fri, 27 Mar 2026 19:13:22 +0800 Subject: [PATCH 8/8] 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) --- docs/paper/reductions.typ | 117 ++++++++++++++++++++++++++++++-------- 1 file changed, 94 insertions(+), 23 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 6f29860cf..de79d7edb 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -5125,34 +5125,105 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], #{ let x = load-model-example("JobShopScheduling") - let blocks = ( - (0, 0, 0, 0, 3), - (0, 1, 1, 3, 6), - (0, 2, 0, 6, 10), - (0, 3, 1, 10, 12), - (0, 4, 0, 12, 14), - (0, 4, 2, 17, 18), - (1, 1, 0, 0, 2), - (1, 3, 0, 2, 7), - (1, 0, 1, 7, 11), - (1, 2, 1, 11, 14), - (1, 4, 1, 14, 17), - (1, 1, 2, 17, 19), - ) - let makespan = 19 + let jobs = x.instance.jobs + let m = x.instance.num_processors + let n = jobs.len() + let lehmer = x.optimal_config + + // Flatten tasks: build per-machine task lists and lengths + let task-lengths = () + let task-job = () // which job each flat task belongs to + let task-index = () // which task within the job + let machine-tasks = range(m).map(_ => ()) + let tid = 0 + for (ji, job) in jobs.enumerate() { + for (ki, op) in job.enumerate() { + let (mi, len) = op + task-lengths.push(len) + task-job.push(ji) + task-index.push(ki) + machine-tasks.at(mi).push(tid) + tid += 1 + } + } + let T = task-lengths.len() + + // Decode per-machine Lehmer codes into machine orders + let offset = 0 + let machine-orders = () + for mi in range(m) { + let mt = machine-tasks.at(mi) + let k = mt.len() + let seg = lehmer.slice(offset, offset + k) + let avail = range(k) + let order = () + for c in seg { + order.push(mt.at(avail.at(c))) + avail = avail.enumerate().filter(((i, v)) => i != c).map(((i, v)) => v) + } + machine-orders.push(order) + offset += k + } + + // Build DAG edges (job precedence + machine order) + let successors = range(T).map(_ => ()) + let indegree = range(T).map(_ => 0) + // Job precedence edges + let job-task-start = 0 + for job in jobs { + for i in range(job.len() - 1) { + let u = job-task-start + i + let v = job-task-start + i + 1 + successors.at(u).push(v) + indegree.at(v) += 1 + } + job-task-start += job.len() + } + // Machine order edges + for order in machine-orders { + for i in range(order.len() - 1) { + let u = order.at(i) + let v = order.at(i + 1) + successors.at(u).push(v) + indegree.at(v) += 1 + } + } + + // Topological sort + longest-path to compute start times + let start-times = range(T).map(_ => 0) + let queue = () + for t in range(T) { + if indegree.at(t) == 0 { queue.push(t) } + } + while queue.len() > 0 { + let u = queue.remove(0) + let finish = start-times.at(u) + task-lengths.at(u) + for v in successors.at(u) { + if finish > start-times.at(v) { start-times.at(v) = finish } + indegree.at(v) -= 1 + if indegree.at(v) == 0 { queue.push(v) } + } + } + + // Build Gantt blocks: (machine, job, task-within-job, start, end) + let blocks = () + for t in range(T) { + let (mi, _len) = jobs.at(task-job.at(t)).at(task-index.at(t)) + blocks.push((mi, task-job.at(t), task-index.at(t), start-times.at(t), start-times.at(t) + task-lengths.at(t))) + } + let makespan = calc.max(..range(T).map(t => start-times.at(t) + task-lengths.at(t))) [ #problem-def("JobShopScheduling")[ 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. ][ 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.]. - *Example.* The canonical fixture has two machines and five jobs + *Example.* The canonical fixture has #m machines and #n jobs $ - J_1 = ((M_1, 3), (M_2, 4)), - J_2 = ((M_2, 2), (M_1, 3), (M_2, 2)), - J_3 = ((M_1, 4), (M_2, 3)), - J_4 = ((M_2, 5), (M_1, 2)), - J_5 = ((M_1, 2), (M_2, 3), (M_1, 1)). + #for (ji, job) in jobs.enumerate() { + $J_#(ji+1) = (#job.map(((mi, len)) => $(M_#(mi+1), #len)$).join($,$))$ + if ji < n - 1 [$,$] else [.] + } $ 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$. @@ -5170,7 +5241,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let row-h = 0.6 let gap = 0.15 - for mi in range(2) { + for mi in range(m) { let y = -mi * (row-h + gap) content((-0.8, y), text(8pt, "M" + str(mi + 1))) } @@ -5189,7 +5260,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], content(((x0 + x1) / 2, y), text(6pt, "j" + str(ji + 1) + "." + str(ti + 1))) } - let y-axis = -(2 - 1) * (row-h + gap) - row-h / 2 - 0.2 + let y-axis = -(m - 1) * (row-h + gap) - row-h / 2 - 0.2 line((0, y-axis), (makespan * scale, y-axis), stroke: 0.4pt) for t in range(calc.ceil(makespan / 5) + 1).map(i => calc.min(i * 5, makespan)) { let x = t * scale