From 162ff2a81b81a8863a7233259a9c29720b694e99 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 23 Mar 2026 16:34:19 +0800 Subject: [PATCH 1/3] 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 00000000..e87837ce --- /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/3] 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 5b94df3f..4c402841 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 54dcf8ee..ef8c3440 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 9c65f926..f05ab659 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 94817e75..ea0d8a20 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 56b6902e..2e21f45c 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 00000000..e8acf831 --- /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 2d07dc9b..db6ac731 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 f45862e0..9e40f051 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/3] 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 e87837ce..00000000 --- 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.