From a208d27692dc294df74abfd9fd3f01f8680d16d1 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 21:48:14 +0800 Subject: [PATCH 1/5] Add plan for #498: [Model] SequencingToMinimizeWeightedTardiness --- ...quencing-to-minimize-weighted-tardiness.md | 270 ++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100644 docs/plans/2026-03-16-sequencing-to-minimize-weighted-tardiness.md diff --git a/docs/plans/2026-03-16-sequencing-to-minimize-weighted-tardiness.md b/docs/plans/2026-03-16-sequencing-to-minimize-weighted-tardiness.md new file mode 100644 index 000000000..86928d88b --- /dev/null +++ b/docs/plans/2026-03-16-sequencing-to-minimize-weighted-tardiness.md @@ -0,0 +1,270 @@ +# SequencingToMinimizeWeightedTardiness Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add the `SequencingToMinimizeWeightedTardiness` model as a decision/satisfaction problem with brute-force-compatible permutation encoding, CLI creation support, canonical example data, tests, and paper documentation. + +**Architecture:** Implement a new `misc` model that follows the Lehmer-code permutation pattern already used by `FlowShopScheduling` and `MinimumTardinessSequencing`. The model stores `lengths`, `weights`, `deadlines`, and `bound`, decodes schedule permutations, computes total weighted tardiness for the induced single-machine schedule, and evaluates to `true` exactly when the permutation is valid and the total weighted tardiness is at most `bound`. Batch 1 covers code, registry, CLI, example-db, and tests. Batch 2 covers the Typst paper entry and bibliography, after Batch 1 makes the canonical example export available. + +**Tech Stack:** Rust workspace (`problemreductions` and `problemreductions-cli`), serde/inventory registry, brute-force solver, Typst paper, GitHub issue `#498`, companion rule issue `#473`. + +--- + +## Issue Notes + +- Treat this as the decision form from Garey & Johnson SS5: given lengths `l_j`, weights `w_j`, deadlines `d_j`, and bound `K`, ask whether some one-machine schedule has total weighted tardiness at most `K`. +- Reuse the issue's 5-task example data, but fix the bound inconsistency called out in the issue comments. +- Brute-force verification on the issue instance shows a unique optimal schedule `(t_1, t_2, t_5, t_4, t_3)` with total weighted tardiness `13`. +- Use `K = 13` for the canonical YES example and `K = 12` for the corresponding NO test. +- Associated rule issue already exists: `#473 [Rule] 3-Partition to Sequencing to Minimize Weighted Tardiness`, so this model is not orphaned. + +## Batch 1: Model, CLI, Example-DB, Tests + +### Task 1: Write the failing model tests first + +**Files:** +- Create: `src/unit_tests/models/misc/sequencing_to_minimize_weighted_tardiness.rs` +- Modify: `src/unit_tests/trait_consistency.rs` + +**Step 1: Write the failing test file** + +Cover the concrete behaviors below before any production code exists: +- construction/accessors/dims/problem metadata +- total weighted tardiness helper on the issue example +- YES evaluation for the optimal Lehmer code `[0, 0, 2, 1, 0]` with `K = 13` +- NO evaluation for the same schedule under `K = 12` +- invalid configs: wrong length and invalid Lehmer digits +- brute-force solver returns a satisfying schedule for `K = 13` and none for `K = 12` +- serde round-trip +- trait consistency registration entry + +**Step 2: Run the focused tests to verify RED** + +Run: `cargo test sequencing_to_minimize_weighted_tardiness --lib` + +Expected: compile failure because the new model module and test target do not exist yet. + +**Step 3: Add the trait-consistency failing assertions** + +Add: +- `check_problem_trait(&SequencingToMinimizeWeightedTardiness::new(...), "SequencingToMinimizeWeightedTardiness")` + +Run: `cargo test trait_consistency --lib` + +Expected: compile failure because the type is not defined or not re-exported yet. + +**Step 4: Commit the red state if the branch policy allows partial commits** + +```bash +git add src/unit_tests/models/misc/sequencing_to_minimize_weighted_tardiness.rs src/unit_tests/trait_consistency.rs +git commit -m "test: add failing tests for weighted tardiness sequencing" +``` + +### Task 2: Implement the new model and core wiring + +**Files:** +- Create: `src/models/misc/sequencing_to_minimize_weighted_tardiness.rs` +- Modify: `src/models/misc/mod.rs` +- Modify: `src/models/mod.rs` +- Modify: `src/lib.rs` + +**Step 1: Implement the model with the smallest code needed to satisfy Task 1** + +The new file should include: +- `ProblemSchemaEntry` with fields `lengths`, `weights`, `deadlines`, `bound` +- struct definition with serde derives +- constructor `new(lengths: Vec, weights: Vec, deadlines: Vec, bound: u64)` +- accessors `lengths()`, `weights()`, `deadlines()`, `bound()`, `num_tasks()` +- helper(s) to decode Lehmer code into job order +- helper `total_weighted_tardiness(&self, config: &[usize]) -> Option` or equivalent safe numeric return +- `Problem` impl with `type Metric = bool`, `dims() = [n, n-1, ..., 1]`, and `evaluate()` returning `true` iff the config is a valid Lehmer permutation with total weighted tardiness `<= bound` +- `SatisfactionProblem` impl +- `declare_variants! { default sat SequencingToMinimizeWeightedTardiness => "factorial(num_tasks)", }` +- `#[cfg(test)]` link to the new unit test file +- `#[cfg(feature = "example-db")]` canonical example spec using the issue instance and the optimal config `[0, 0, 2, 1, 0]` + +**Step 2: Wire the model into exports** + +Update module/re-export files so the type is visible through: +- `crate::models::misc::*` +- `crate::models::*` +- `crate::prelude::*` + +Also extend `src/models/misc/mod.rs` so the example-db aggregator includes the new canonical example spec. + +**Step 3: Run the focused tests to reach GREEN** + +Run: +- `cargo test sequencing_to_minimize_weighted_tardiness --lib` +- `cargo test trait_consistency --lib` + +Expected: the new tests and trait-consistency entries pass. + +**Step 4: Refactor only after green** + +If needed, extract small private helpers so the model matches the style of `FlowShopScheduling` and `MinimumTardinessSequencing`. Do not add solver features beyond what the tests require. + +**Step 5: Commit** + +```bash +git add src/models/misc/sequencing_to_minimize_weighted_tardiness.rs src/models/misc/mod.rs src/models/mod.rs src/lib.rs src/unit_tests/models/misc/sequencing_to_minimize_weighted_tardiness.rs src/unit_tests/trait_consistency.rs +git commit -m "feat: add sequencing to minimize weighted tardiness model" +``` + +### Task 3: Add CLI creation support and user-facing help + +**Files:** +- Modify: `problemreductions-cli/src/commands/create.rs` +- Modify: `problemreductions-cli/src/cli.rs` + +**Step 1: Write or extend the failing CLI-facing tests if nearby coverage exists** + +If there is an established CLI create test location for similar models, add a focused regression there. If not, keep this task implementation-focused and rely on targeted command execution for verification. + +**Step 2: Add the create-arm** + +Implement `pred create SequencingToMinimizeWeightedTardiness` using existing flags: +- `--sizes` for processing lengths +- `--weights` for tardiness weights +- `--deadlines` for job deadlines +- `--bound` for the decision threshold + +Requirements: +- parse all four inputs +- require equal vector lengths +- convert `--bound` from the existing integer flag type to `u64` with a clear error if negative +- instantiate `SequencingToMinimizeWeightedTardiness::new(...)` + +**Step 3: Update help text** + +Add the new problem to the `CreateArgs` after-help table with the exact flag combination above. No new CLI flags should be introduced for this model. + +**Step 4: Verify the CLI path** + +Run: +- `cargo test create --package problemreductions-cli` +- `cargo run -p problemreductions-cli -- create SequencingToMinimizeWeightedTardiness --sizes 3,4,2,5,3 --weights 2,3,1,4,2 --deadlines 5,8,4,15,10 --bound 13` + +Expected: +- tests stay green +- the command emits JSON for the new model with the expected fields + +**Step 5: Commit** + +```bash +git add problemreductions-cli/src/commands/create.rs problemreductions-cli/src/cli.rs +git commit -m "feat: add CLI creation for weighted tardiness sequencing" +``` + +### Task 4: Finalize Batch 1 verification + +**Files:** +- Review only: all files touched in Tasks 1-3 + +**Step 1: Run the model-level verification suite** + +Run: +- `cargo test sequencing_to_minimize_weighted_tardiness --lib` +- `cargo test trait_consistency --lib` +- `cargo test example_db --lib --features example-db` + +Expected: all pass. + +**Step 2: Run the repo quick check if Batch 1 is stable** + +Run: `make check` + +Expected: format, clippy, and test checks pass for the workspace. + +**Step 3: Note any generated-file changes** + +If verification regenerates tracked exports, inspect them and keep only expected changes related to the new model. + +## Batch 2: Paper Entry and Citation Wiring + +### Task 5: Add the paper entry and bibliography + +**Files:** +- Modify: `docs/paper/reductions.typ` +- Modify: `docs/paper/references.bib` +- Test: `src/unit_tests/models/misc/sequencing_to_minimize_weighted_tardiness.rs` + +**Step 1: Add bibliography entries** + +Add the citations needed by the paper text if they are missing: +- Garey & Johnson book entry already exists; reuse it +- Lawler 1977 reference for strong NP-hardness / special equal-weight case +- Potts & Van Wassenhove 1985 branch-and-bound +- Tanaka, Fujikuma, and Araki 2009 exact algorithm + +**Step 2: Register the display name** + +Add: + +```typst +"SequencingToMinimizeWeightedTardiness": [Sequencing to Minimize Weighted Tardiness], +``` + +**Step 3: Write the `problem-def(...)` entry** + +Place it in the scheduling section near `MinimumTardinessSequencing`. The entry should: +- define the decision problem with lengths, weights, deadlines, and bound `K` +- mention standard notation `1 || sum w_j T_j` +- explain strong NP-hardness and the key tractable special cases from the issue context +- use the canonical example data and the optimal order `(t_1, t_2, t_5, t_4, t_3)` +- show that the completion times are `(3, 7, 10, 15, 17)` and only `t_3` is tardy, contributing `13` +- make the example visually clear from the start instead of changing `K` midway + +**Step 4: Add the paper-example regression test** + +Extend `src/unit_tests/models/misc/sequencing_to_minimize_weighted_tardiness.rs` with a `test_sequencing_to_minimize_weighted_tardiness_paper_example` that: +- constructs the exact paper instance +- asserts the canonical config `[0, 0, 2, 1, 0]` is satisfying for `K = 13` +- asserts the same ordering is not satisfying for `K = 12` +- uses brute force to confirm the YES instance has at least one satisfying schedule and the NO instance has none + +**Step 5: Verify the paper batch** + +Run: +- `cargo test sequencing_to_minimize_weighted_tardiness --lib` +- `make paper` + +Expected: tests pass and the Typst paper builds successfully. + +**Step 6: Commit** + +```bash +git add docs/paper/reductions.typ docs/paper/references.bib src/unit_tests/models/misc/sequencing_to_minimize_weighted_tardiness.rs +git commit -m "docs: add paper entry for weighted tardiness sequencing" +``` + +## Final Verification and Review Handoff + +### Task 6: Full repo verification and review preparation + +**Files:** +- Review only: all touched files + +**Step 1: Run final verification** + +Run: +- `make check` +- `make paper` + +If runtime is acceptable, also run: +- `make test` + +**Step 2: Run implementation review** + +Invoke the repo-local review skill after code is complete: +- `review-implementation` + +Fix structural or Important quality findings before the implementation summary is posted to the PR. + +**Step 3: Prepare the PR summary** + +Call out: +- decision-model choice (not optimization-model choice) +- corrected canonical example (`K = 13` YES, `K = 12` NO) +- CLI flags used for construction +- any deviations from this plan From 3e84bc33e3df939de71113d88b2bb7c9c2c87ee0 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 22:05:13 +0800 Subject: [PATCH 2/5] Implement #498: [Model] SequencingToMinimizeWeightedTardiness --- docs/paper/reductions.typ | 82 ++++++++ docs/paper/references.bib | 32 +++ docs/src/reductions/problem_schemas.json | 26 +++ docs/src/reductions/reduction_graph.json | 27 ++- problemreductions-cli/src/cli.rs | 1 + problemreductions-cli/src/commands/create.rs | 61 +++++- problemreductions-cli/tests/cli_tests.rs | 81 +++++++ src/example_db/fixtures/examples.json | 1 + src/lib.rs | 3 +- src/models/misc/mod.rs | 4 + ...quencing_to_minimize_weighted_tardiness.rs | 197 ++++++++++++++++++ src/models/mod.rs | 3 +- ...quencing_to_minimize_weighted_tardiness.rs | 119 +++++++++++ src/unit_tests/trait_consistency.rs | 4 + 14 files changed, 628 insertions(+), 13 deletions(-) create mode 100644 src/models/misc/sequencing_to_minimize_weighted_tardiness.rs create mode 100644 src/unit_tests/models/misc/sequencing_to_minimize_weighted_tardiness.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index b14ef6ea0..743d20757 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -106,6 +106,7 @@ "PartitionIntoTriangles": [Partition Into Triangles], "FlowShopScheduling": [Flow Shop Scheduling], "MinimumTardinessSequencing": [Minimum Tardiness Sequencing], + "SequencingToMinimizeWeightedTardiness": [Sequencing to Minimize Weighted Tardiness], ) // Definition label: "def:" — each definition block must have a matching label @@ -1977,6 +1978,87 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS ] } +#{ + let x = load-model-example("SequencingToMinimizeWeightedTardiness") + let lengths = x.instance.lengths + let weights = x.instance.weights + let deadlines = x.instance.deadlines + let bound = x.instance.bound + let njobs = lengths.len() + let sol = x.optimal.at(0) + let lehmer = sol.config + let schedule = { + let avail = range(njobs) + let result = () + for c in lehmer { + result.push(avail.at(c)) + avail = avail.enumerate().filter(((i, v)) => i != c).map(((i, v)) => v) + } + result + } + let completions = { + let t = 0 + let result = () + for job in schedule { + t += lengths.at(job) + result.push(t) + } + result + } + let tardiness = schedule.enumerate().map(((pos, job)) => calc.max(0, completions.at(pos) - deadlines.at(job))) + let weighted = schedule.enumerate().map(((pos, job)) => tardiness.at(pos) * weights.at(job)) + let total-weighted = weighted.fold(0, (acc, v) => acc + v) + let tardy-jobs = schedule.enumerate().filter(((pos, job)) => tardiness.at(pos) > 0).map(((pos, job)) => job) + [ + #problem-def("SequencingToMinimizeWeightedTardiness")[ + Given a set $J$ of $n$ jobs, processing times $ell_j in ZZ^+$, tardiness weights $w_j in ZZ^+$, deadlines $d_j in ZZ^+$, and a bound $K in ZZ^+$, determine whether there exists a one-machine schedule whose total weighted tardiness + $sum_(j in J) w_j max(0, C_j - d_j)$ + is at most $K$, where $C_j$ is the completion time of job $j$. + ][ + Sequencing to Minimize Weighted Tardiness is the classical single-machine scheduling problem $1 || sum w_j T_j$, where $T_j = max(0, C_j - d_j)$. It appears as SS5 in Garey & Johnson @garey1979 and is strongly NP-complete via transformation from 3-Partition, which rules out pseudo-polynomial algorithms in general. When all weights are equal, the special case reduces to ordinary total tardiness and admits a pseudo-polynomial dynamic program @lawler1977. Garey & Johnson also note that the equal-length case is polynomial-time solvable by bipartite matching @garey1979. + + Exact algorithms remain exponential in the worst case. Brute-force over all $n!$ schedules evaluates the implementation's decision encoding in $O(n! dot n)$ time. More refined exact methods include the branch-and-bound algorithm of Potts and Van Wassenhove @potts1985 and the dynamic-programming style exact algorithm of Tanaka, Fujikuma, and Araki @tanaka2009. + + *Example.* Consider the five jobs with processing times $ell = (#lengths.map(v => str(v)).join(", "))$, weights $w = (#weights.map(v => str(v)).join(", "))$, deadlines $d = (#deadlines.map(v => str(v)).join(", "))$, and bound $K = #bound$. The unique satisfying schedule is $(#schedule.map(job => $t_#(job + 1)$).join(", "))$, with completion times $(#completions.map(v => str(v)).join(", "))$. Only job $t_#(tardy-jobs.at(0) + 1)$ is tardy; the per-job weighted tardiness contributions are $(#weighted.map(v => str(v)).join(", "))$, so the total weighted tardiness is $#total-weighted <= K$. + + #figure( + canvas(length: 1cm, { + import draw: * + let colors = (rgb("#4e79a7"), rgb("#e15759"), rgb("#76b7b2"), rgb("#f28e2b"), rgb("#59a14f")) + let scale = 0.34 + let row-h = 0.7 + let y = 0 + + for (pos, job) in schedule.enumerate() { + let start = if pos == 0 { 0 } else { completions.at(pos - 1) } + let end = completions.at(pos) + let is-tardy = tardiness.at(pos) > 0 + let fill = colors.at(calc.rem(job, colors.len())).transparentize(if is-tardy { 70% } else { 30% }) + let stroke = colors.at(calc.rem(job, colors.len())) + rect((start * scale, y - row-h / 2), (end * scale, y + row-h / 2), + fill: fill, stroke: 0.4pt + stroke) + content(((start + end) * scale / 2, y), text(7pt, $t_#(job + 1)$)) + + let dl = deadlines.at(job) + line((dl * scale, y + row-h / 2 + 0.05), (dl * scale, y + row-h / 2 + 0.2), + stroke: (paint: if is-tardy { red } else { green.darken(20%) }, thickness: 0.6pt)) + } + + let axis-y = -row-h / 2 - 0.25 + line((0, axis-y), (completions.at(completions.len() - 1) * scale, axis-y), stroke: 0.4pt) + for t in range(completions.at(completions.len() - 1) + 1) { + let x = t * scale + line((x, axis-y), (x, axis-y - 0.08), stroke: 0.4pt) + content((x, axis-y - 0.22), text(6pt, str(t))) + } + content((completions.at(completions.len() - 1) * scale / 2, axis-y - 0.42), text(7pt)[time]) + }), + caption: [Single-machine schedule for the canonical weighted-tardiness example. The faded job is tardy; colored ticks mark the individual deadlines $d_j$.], + ) + ] + ] +} + // Completeness check: warn about problem types in JSON but missing from paper #{ let json-models = { diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 53e868b84..a5dee4116 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -40,6 +40,38 @@ @article{shang2018 doi = {10.1016/j.cor.2017.10.015} } +@article{lawler1977, + author = {Eugene L. Lawler}, + title = {A pseudopolynomial algorithm for sequencing jobs to minimize total tardiness}, + journal = {Annals of Discrete Mathematics}, + volume = {1}, + pages = {331--342}, + year = {1977}, + doi = {10.1016/S0167-5060(08)70742-8} +} + +@article{potts1985, + author = {Chris N. Potts and Luk N. Van Wassenhove}, + title = {A Branch and Bound Algorithm for the Total Weighted Tardiness Problem}, + journal = {Operations Research}, + volume = {33}, + number = {2}, + pages = {363--377}, + year = {1985}, + doi = {10.1287/opre.33.2.363} +} + +@article{tanaka2009, + author = {Shunji Tanaka and Shuji Fujikuma and Mituhiko Araki}, + title = {An exact algorithm for single-machine scheduling without machine idle time}, + journal = {Journal of Scheduling}, + volume = {12}, + number = {6}, + pages = {575--593}, + year = {2009}, + doi = {10.1007/s10951-008-0093-5} +} + @inproceedings{karp1972, author = {Richard M. Karp}, title = {Reducibility among Combinatorial Problems}, diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index 783651a32..03d77b8df 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -594,6 +594,32 @@ } ] }, + { + "name": "SequencingToMinimizeWeightedTardiness", + "description": "Schedule jobs on one machine so total weighted tardiness is at most K", + "fields": [ + { + "name": "lengths", + "type_name": "Vec", + "description": "Processing times l_j for each job" + }, + { + "name": "weights", + "type_name": "Vec", + "description": "Tardiness weights w_j for each job" + }, + { + "name": "deadlines", + "type_name": "Vec", + "description": "Deadlines d_j for each job" + }, + { + "name": "bound", + "type_name": "u64", + "description": "Upper bound K on total weighted tardiness" + } + ] + }, { "name": "SetBasis", "description": "Determine whether a collection of sets admits a basis of size k under union", diff --git a/docs/src/reductions/reduction_graph.json b/docs/src/reductions/reduction_graph.json index 8488ef012..bbf8c0f52 100644 --- a/docs/src/reductions/reduction_graph.json +++ b/docs/src/reductions/reduction_graph.json @@ -464,6 +464,13 @@ "doc_path": "models/formula/struct.Satisfiability.html", "complexity": "2^num_variables" }, + { + "name": "SequencingToMinimizeWeightedTardiness", + "variant": {}, + "category": "misc", + "doc_path": "models/misc/struct.SequencingToMinimizeWeightedTardiness.html", + "complexity": "factorial(num_tasks)" + }, { "name": "SetBasis", "variant": {}, @@ -576,7 +583,7 @@ }, { "source": 4, - "target": 55, + "target": 56, "overhead": [ { "field": "num_spins", @@ -740,7 +747,7 @@ }, { "source": 21, - "target": 59, + "target": 60, "overhead": [ { "field": "num_elements", @@ -796,7 +803,7 @@ }, { "source": 25, - "target": 55, + "target": 56, "overhead": [ { "field": "num_spins", @@ -1242,7 +1249,7 @@ }, { "source": 49, - "target": 54, + "target": 55, "overhead": [ { "field": "num_spins", @@ -1327,7 +1334,7 @@ "doc_path": "rules/sat_minimumdominatingset/index.html" }, { - "source": 54, + "source": 55, "target": 49, "overhead": [ { @@ -1338,7 +1345,7 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 55, + "source": 56, "target": 25, "overhead": [ { @@ -1353,8 +1360,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 55, - "target": 54, + "source": 56, + "target": 55, "overhead": [ { "field": "num_spins", @@ -1368,7 +1375,7 @@ "doc_path": "rules/spinglass_casts/index.html" }, { - "source": 60, + "source": 61, "target": 12, "overhead": [ { @@ -1383,7 +1390,7 @@ "doc_path": "rules/travelingsalesman_ilp/index.html" }, { - "source": 60, + "source": 61, "target": 49, "overhead": [ { diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 919dffd3d..1e905d092 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -244,6 +244,7 @@ Flags by problem type: FVS --arcs [--weights] [--num-vertices] FlowShopScheduling --task-lengths, --deadline [--num-processors] MinimumTardinessSequencing --n, --deadlines [--precedence-pairs] + SequencingToMinimizeWeightedTardiness --sizes, --weights, --deadlines, --bound SCS --strings, --bound [--alphabet-size] ILP, CircuitSAT (via reduction only) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index cf330a93f..90e91cdcc 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -9,7 +9,7 @@ use problemreductions::models::algebraic::{ClosestVectorProblem, BMF}; use problemreductions::models::graph::{GraphPartitioning, HamiltonianPath}; use problemreductions::models::misc::{ BinPacking, FlowShopScheduling, LongestCommonSubsequence, MinimumTardinessSequencing, - PaintShop, ShortestCommonSupersequence, SubsetSum, + PaintShop, SequencingToMinimizeWeightedTardiness, ShortestCommonSupersequence, SubsetSum, }; use problemreductions::prelude::*; use problemreductions::registry::collect_schemas; @@ -262,6 +262,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "--graph 0-1,1-2,2-3,3-0 --edge-weights 1,1,1,1 --required-edges 0,2 --bound 4" } "SubgraphIsomorphism" => "--graph 0-1,1-2,2-0 --pattern 0-1", + "SequencingToMinimizeWeightedTardiness" => { + "--sizes 3,4,2,5,3 --weights 2,3,1,4,2 --deadlines 5,8,4,15,10 --bound 13" + } "SubsetSum" => "--sizes 3,7,1,8,2,4 --target 11", "SetBasis" => "--universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3", "ShortestCommonSupersequence" => "--strings \"0,1,2;1,2,0\" --bound 4", @@ -914,6 +917,62 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // SequencingToMinimizeWeightedTardiness + "SequencingToMinimizeWeightedTardiness" => { + let sizes_str = args.sizes.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "SequencingToMinimizeWeightedTardiness requires --sizes, --weights, --deadlines, and --bound\n\n\ + Usage: pred create SequencingToMinimizeWeightedTardiness --sizes 3,4,2,5,3 --weights 2,3,1,4,2 --deadlines 5,8,4,15,10 --bound 13" + ) + })?; + let weights_str = args.weights.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "SequencingToMinimizeWeightedTardiness requires --weights (comma-separated tardiness weights)\n\n\ + Usage: pred create SequencingToMinimizeWeightedTardiness --sizes 3,4,2,5,3 --weights 2,3,1,4,2 --deadlines 5,8,4,15,10 --bound 13" + ) + })?; + let deadlines_str = args.deadlines.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "SequencingToMinimizeWeightedTardiness requires --deadlines (comma-separated job deadlines)\n\n\ + Usage: pred create SequencingToMinimizeWeightedTardiness --sizes 3,4,2,5,3 --weights 2,3,1,4,2 --deadlines 5,8,4,15,10 --bound 13" + ) + })?; + let bound = args.bound.ok_or_else(|| { + anyhow::anyhow!( + "SequencingToMinimizeWeightedTardiness requires --bound\n\n\ + Usage: pred create SequencingToMinimizeWeightedTardiness --sizes 3,4,2,5,3 --weights 2,3,1,4,2 --deadlines 5,8,4,15,10 --bound 13" + ) + })?; + anyhow::ensure!(bound >= 0, "--bound must be non-negative"); + + let lengths: Vec = util::parse_comma_list(sizes_str)?; + let weights: Vec = util::parse_comma_list(weights_str)?; + let deadlines: Vec = util::parse_comma_list(deadlines_str)?; + + anyhow::ensure!( + lengths.len() == weights.len(), + "sizes length ({}) must equal weights length ({})", + lengths.len(), + weights.len() + ); + anyhow::ensure!( + lengths.len() == deadlines.len(), + "sizes length ({}) must equal deadlines length ({})", + lengths.len(), + deadlines.len() + ); + + ( + ser(SequencingToMinimizeWeightedTardiness::new( + lengths, + weights, + deadlines, + bound as u64, + ))?, + resolved_variant.clone(), + ) + } + // OptimalLinearArrangement — graph + bound "OptimalLinearArrangement" => { let (graph, _) = parse_graph(args).map_err(|e| { diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 3855045f6..7de25f94d 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -688,6 +688,72 @@ fn test_create_set_basis_rejects_out_of_range_elements() { assert!(!stderr.contains("panicked at"), "stderr: {stderr}"); } +#[test] +fn test_create_sequencing_to_minimize_weighted_tardiness() { + let output_file = + std::env::temp_dir().join("pred_test_create_weighted_tardiness_sequencing.json"); + let output = pred() + .args([ + "-o", + output_file.to_str().unwrap(), + "create", + "SequencingToMinimizeWeightedTardiness", + "--sizes", + "3,4,2,5,3", + "--weights", + "2,3,1,4,2", + "--deadlines", + "5,8,4,15,10", + "--bound", + "13", + ]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let content = std::fs::read_to_string(&output_file).unwrap(); + let json: serde_json::Value = serde_json::from_str(&content).unwrap(); + assert_eq!(json["type"], "SequencingToMinimizeWeightedTardiness"); + assert_eq!(json["data"]["lengths"], serde_json::json!([3, 4, 2, 5, 3])); + assert_eq!(json["data"]["weights"], serde_json::json!([2, 3, 1, 4, 2])); + assert_eq!( + json["data"]["deadlines"], + serde_json::json!([5, 8, 4, 15, 10]) + ); + assert_eq!(json["data"]["bound"], 13); + + std::fs::remove_file(&output_file).ok(); +} + +#[test] +fn test_create_sequencing_to_minimize_weighted_tardiness_rejects_mismatched_lengths() { + let output = pred() + .args([ + "create", + "SequencingToMinimizeWeightedTardiness", + "--sizes", + "3,4,2", + "--weights", + "2,3", + "--deadlines", + "5,8,4", + "--bound", + "13", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("sizes length (3) must equal weights length (2)"), + "stderr: {stderr}" + ); +} + #[test] fn test_create_then_evaluate() { // Create a problem @@ -1559,6 +1625,21 @@ fn test_create_no_flags_shows_help() { ); } +#[test] +fn test_create_sequencing_to_minimize_weighted_tardiness_no_flags_shows_help() { + let output = pred() + .args(["create", "SequencingToMinimizeWeightedTardiness"]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("--sizes")); + assert!(stderr.contains("--weights")); + assert!(stderr.contains("--deadlines")); + assert!(stderr.contains("--bound")); + assert!(stderr.contains("pred create SequencingToMinimizeWeightedTardiness")); +} + #[test] fn test_create_set_basis_no_flags_uses_actual_cli_flag_names() { let output = pred().args(["create", "SetBasis"]).output().unwrap(); diff --git a/src/example_db/fixtures/examples.json b/src/example_db/fixtures/examples.json index b3f46deb6..e0aebae9e 100644 --- a/src/example_db/fixtures/examples.json +++ b/src/example_db/fixtures/examples.json @@ -28,6 +28,7 @@ {"problem":"PartitionIntoTriangles","variant":{"graph":"SimpleGraph"},"instance":{"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[1,2,null],[3,4,null],[3,5,null],[4,5,null],[0,3,null]],"node_holes":[],"nodes":[null,null,null,null,null,null]}}},"samples":[{"config":[0,0,0,1,1,1],"metric":true}],"optimal":[{"config":[0,0,0,1,1,1],"metric":true},{"config":[1,1,1,0,0,0],"metric":true}]}, {"problem":"QUBO","variant":{"weight":"f64"},"instance":{"matrix":[[-1.0,2.0,0.0],[0.0,-1.0,2.0],[0.0,0.0,-1.0]],"num_vars":3},"samples":[{"config":[1,0,1],"metric":{"Valid":-2.0}}],"optimal":[{"config":[1,0,1],"metric":{"Valid":-2.0}}]}, {"problem":"Satisfiability","variant":{},"instance":{"clauses":[{"literals":[1,2]},{"literals":[-1,3]},{"literals":[-2,-3]}],"num_vars":3},"samples":[{"config":[1,0,1],"metric":true}],"optimal":[{"config":[0,1,0],"metric":true},{"config":[1,0,1],"metric":true}]}, + {"problem":"SequencingToMinimizeWeightedTardiness","variant":{},"instance":{"bound":13,"deadlines":[5,8,4,15,10],"lengths":[3,4,2,5,3],"weights":[2,3,1,4,2]},"samples":[{"config":[0,0,2,1,0],"metric":true}],"optimal":[{"config":[0,0,2,1,0],"metric":true}]}, {"problem":"SetBasis","variant":{},"instance":{"collection":[[0,1],[1,2],[0,2],[0,1,2]],"k":3,"universe_size":4},"samples":[{"config":[1,0,0,0,0,1,0,0,0,0,1,0],"metric":true}],"optimal":[{"config":[0,0,1,0,0,1,0,0,1,0,0,0],"metric":true},{"config":[0,0,1,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,1,0,0,0,0,1,0,1,0,0,0],"metric":true},{"config":[0,1,0,0,1,0,0,0,0,0,1,0],"metric":true},{"config":[0,1,1,0,1,0,1,0,1,1,0,0],"metric":true},{"config":[0,1,1,0,1,1,0,0,1,0,1,0],"metric":true},{"config":[1,0,0,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,0,0,0,0,1,0,0,0,0,1,0],"metric":true},{"config":[1,0,1,0,0,1,1,0,1,1,0,0],"metric":true},{"config":[1,0,1,0,1,1,0,0,0,1,1,0],"metric":true},{"config":[1,1,0,0,0,1,1,0,1,0,1,0],"metric":true},{"config":[1,1,0,0,1,0,1,0,0,1,1,0],"metric":true}]}, {"problem":"ShortestCommonSupersequence","variant":{},"instance":{"alphabet_size":3,"bound":4,"strings":[[0,1,2],[1,0,2]]},"samples":[{"config":[1,0,1,2],"metric":true}],"optimal":[{"config":[0,1,0,2],"metric":true},{"config":[1,0,1,2],"metric":true}]}, {"problem":"SpinGlass","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"couplings":[1,1,1,1,1,1,1],"fields":[0,0,0,0,0],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[1,2,null],[3,4,null],[0,3,null],[1,3,null],[1,4,null],[2,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}}},"samples":[{"config":[1,0,1,1,0],"metric":{"Valid":-3}}],"optimal":[{"config":[0,0,1,1,0],"metric":{"Valid":-3}},{"config":[0,1,0,0,1],"metric":{"Valid":-3}},{"config":[0,1,0,1,0],"metric":{"Valid":-3}},{"config":[0,1,1,1,0],"metric":{"Valid":-3}},{"config":[1,0,0,0,1],"metric":{"Valid":-3}},{"config":[1,0,1,0,1],"metric":{"Valid":-3}},{"config":[1,0,1,1,0],"metric":{"Valid":-3}},{"config":[1,1,0,0,1],"metric":{"Valid":-3}}]}, diff --git a/src/lib.rs b/src/lib.rs index 2b5c4f4bb..340512b18 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,7 +56,8 @@ pub mod prelude { }; pub use crate::models::misc::{ BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, - MinimumTardinessSequencing, PaintShop, ShortestCommonSupersequence, SubsetSum, + MinimumTardinessSequencing, PaintShop, SequencingToMinimizeWeightedTardiness, + ShortestCommonSupersequence, SubsetSum, }; pub use crate::models::set::{ ExactCoverBy3Sets, MaximumSetPacking, MinimumSetCovering, SetBasis, diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index cc96aa83e..af5037cd9 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -8,6 +8,7 @@ //! - [`LongestCommonSubsequence`]: Longest Common Subsequence //! - [`MinimumTardinessSequencing`]: Minimize tardy tasks in single-machine scheduling //! - [`PaintShop`]: Minimize color switches in paint shop scheduling +//! - [`SequencingToMinimizeWeightedTardiness`]: Decide whether a schedule meets a weighted tardiness bound //! - [`ShortestCommonSupersequence`]: Find a common supersequence of bounded length //! - [`SubsetSum`]: Find a subset summing to exactly a target value @@ -18,6 +19,7 @@ mod knapsack; mod longest_common_subsequence; mod minimum_tardiness_sequencing; pub(crate) mod paintshop; +mod sequencing_to_minimize_weighted_tardiness; pub(crate) mod shortest_common_supersequence; mod subset_sum; @@ -28,6 +30,7 @@ pub use knapsack::Knapsack; pub use longest_common_subsequence::LongestCommonSubsequence; pub use minimum_tardiness_sequencing::MinimumTardinessSequencing; pub use paintshop::PaintShop; +pub use sequencing_to_minimize_weighted_tardiness::SequencingToMinimizeWeightedTardiness; pub use shortest_common_supersequence::ShortestCommonSupersequence; pub use subset_sum::SubsetSum; @@ -38,5 +41,6 @@ pub(crate) fn canonical_model_example_specs() -> Vec", description: "Processing times l_j for each job" }, + FieldInfo { name: "weights", type_name: "Vec", description: "Tardiness weights w_j for each job" }, + FieldInfo { name: "deadlines", type_name: "Vec", description: "Deadlines d_j for each job" }, + FieldInfo { name: "bound", type_name: "u64", description: "Upper bound K on total weighted tardiness" }, + ], + } +} + +/// Sequencing to Minimize Weighted Tardiness. +/// +/// Given jobs with processing times `l_j`, weights `w_j`, deadlines `d_j`, +/// and a bound `K`, determine whether there exists a permutation schedule on a +/// single machine whose total weighted tardiness +/// `sum_j w_j * max(0, C_j - d_j)` is at most `K`, where `C_j` is the +/// completion time of job `j`. +/// +/// # Representation +/// +/// Configurations use Lehmer code to encode permutations of the jobs. +/// Decoding yields the job order processed by the single machine. +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::misc::SequencingToMinimizeWeightedTardiness; +/// use problemreductions::{BruteForce, Problem, Solver}; +/// +/// let problem = SequencingToMinimizeWeightedTardiness::new( +/// vec![3, 4, 2, 5, 3], +/// vec![2, 3, 1, 4, 2], +/// vec![5, 8, 4, 15, 10], +/// 13, +/// ); +/// +/// let solver = BruteForce::new(); +/// assert!(solver.find_satisfying(&problem).is_some()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SequencingToMinimizeWeightedTardiness { + lengths: Vec, + weights: Vec, + deadlines: Vec, + bound: u64, +} + +impl SequencingToMinimizeWeightedTardiness { + /// Create a new weighted tardiness scheduling instance. + /// + /// # Panics + /// + /// Panics if the input vectors do not have the same length. + pub fn new(lengths: Vec, weights: Vec, deadlines: Vec, bound: u64) -> Self { + assert_eq!( + lengths.len(), + weights.len(), + "weights length must equal lengths length" + ); + assert_eq!( + lengths.len(), + deadlines.len(), + "deadlines length must equal lengths length" + ); + Self { + lengths, + weights, + deadlines, + bound, + } + } + + /// Returns the job lengths. + pub fn lengths(&self) -> &[u64] { + &self.lengths + } + + /// Returns the tardiness weights. + pub fn weights(&self) -> &[u64] { + &self.weights + } + + /// Returns the deadlines. + pub fn deadlines(&self) -> &[u64] { + &self.deadlines + } + + /// Returns the weighted tardiness bound. + pub fn bound(&self) -> u64 { + self.bound + } + + /// Returns the number of jobs. + pub fn num_tasks(&self) -> usize { + self.lengths.len() + } + + 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) + } + + fn schedule_weighted_tardiness(&self, schedule: &[usize]) -> Option { + let mut completion_time = 0u128; + let mut total = 0u128; + for &job in schedule { + completion_time += u128::from(self.lengths[job]); + let tardiness = completion_time.saturating_sub(u128::from(self.deadlines[job])); + total += tardiness * u128::from(self.weights[job]); + } + u64::try_from(total).ok() + } + + /// Compute the total weighted tardiness of a Lehmer-encoded schedule. + /// + /// Returns `None` if the configuration is not a valid Lehmer code or if + /// the accumulated objective does not fit in `u64`. + pub fn total_weighted_tardiness(&self, config: &[usize]) -> Option { + let schedule = self.decode_schedule(config)?; + self.schedule_weighted_tardiness(&schedule) + } +} + +impl Problem for SequencingToMinimizeWeightedTardiness { + const NAME: &'static str = "SequencingToMinimizeWeightedTardiness"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + let n = self.num_tasks(); + (0..n).rev().map(|i| i + 1).collect() + } + + fn evaluate(&self, config: &[usize]) -> bool { + self.total_weighted_tardiness(config) + .is_some_and(|total| total <= self.bound) + } +} + +impl SatisfactionProblem for SequencingToMinimizeWeightedTardiness {} + +crate::declare_variants! { + default sat SequencingToMinimizeWeightedTardiness => "factorial(num_tasks)", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "sequencing_to_minimize_weighted_tardiness", + build: || { + let problem = SequencingToMinimizeWeightedTardiness::new( + vec![3, 4, 2, 5, 3], + vec![2, 3, 1, 4, 2], + vec![5, 8, 4, 15, 10], + 13, + ); + crate::example_db::specs::satisfaction_example(problem, vec![vec![0, 0, 2, 1, 0]]) + }, + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/sequencing_to_minimize_weighted_tardiness.rs"] +mod tests; diff --git a/src/models/mod.rs b/src/models/mod.rs index 5395038f3..7ed647c58 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -20,6 +20,7 @@ pub use graph::{ }; pub use misc::{ BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, - MinimumTardinessSequencing, PaintShop, ShortestCommonSupersequence, SubsetSum, + MinimumTardinessSequencing, PaintShop, SequencingToMinimizeWeightedTardiness, + ShortestCommonSupersequence, SubsetSum, }; pub use set::{ExactCoverBy3Sets, MaximumSetPacking, MinimumSetCovering, SetBasis}; diff --git a/src/unit_tests/models/misc/sequencing_to_minimize_weighted_tardiness.rs b/src/unit_tests/models/misc/sequencing_to_minimize_weighted_tardiness.rs new file mode 100644 index 000000000..ca12d961b --- /dev/null +++ b/src/unit_tests/models/misc/sequencing_to_minimize_weighted_tardiness.rs @@ -0,0 +1,119 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::traits::Problem; + +fn issue_example_yes() -> SequencingToMinimizeWeightedTardiness { + SequencingToMinimizeWeightedTardiness::new( + vec![3, 4, 2, 5, 3], + vec![2, 3, 1, 4, 2], + vec![5, 8, 4, 15, 10], + 13, + ) +} + +fn issue_example_no() -> SequencingToMinimizeWeightedTardiness { + SequencingToMinimizeWeightedTardiness::new( + vec![3, 4, 2, 5, 3], + vec![2, 3, 1, 4, 2], + vec![5, 8, 4, 15, 10], + 12, + ) +} + +#[test] +fn test_sequencing_to_minimize_weighted_tardiness_basic() { + let problem = issue_example_yes(); + + assert_eq!(problem.lengths(), &[3, 4, 2, 5, 3]); + assert_eq!(problem.weights(), &[2, 3, 1, 4, 2]); + assert_eq!(problem.deadlines(), &[5, 8, 4, 15, 10]); + assert_eq!(problem.bound(), 13); + assert_eq!(problem.num_tasks(), 5); + assert_eq!(problem.dims(), vec![5, 4, 3, 2, 1]); + assert_eq!( + ::NAME, + "SequencingToMinimizeWeightedTardiness" + ); + assert_eq!( + ::variant(), + vec![] + ); +} + +#[test] +fn test_sequencing_to_minimize_weighted_tardiness_total_weighted_tardiness() { + let problem = issue_example_yes(); + assert_eq!(problem.total_weighted_tardiness(&[0, 0, 2, 1, 0]), Some(13)); +} + +#[test] +fn test_sequencing_to_minimize_weighted_tardiness_evaluate_yes() { + let problem = issue_example_yes(); + assert!(problem.evaluate(&[0, 0, 2, 1, 0])); +} + +#[test] +fn test_sequencing_to_minimize_weighted_tardiness_evaluate_no_with_tighter_bound() { + let problem = issue_example_no(); + assert!(!problem.evaluate(&[0, 0, 2, 1, 0])); +} + +#[test] +fn test_sequencing_to_minimize_weighted_tardiness_invalid_lehmer_digit() { + let problem = issue_example_yes(); + assert_eq!(problem.total_weighted_tardiness(&[0, 0, 3, 0, 0]), None); + assert!(!problem.evaluate(&[0, 0, 3, 0, 0])); +} + +#[test] +fn test_sequencing_to_minimize_weighted_tardiness_wrong_length() { + let problem = issue_example_yes(); + assert_eq!(problem.total_weighted_tardiness(&[0, 0, 2, 1]), None); + assert!(!problem.evaluate(&[0, 0, 2, 1])); +} + +#[test] +fn test_sequencing_to_minimize_weighted_tardiness_solver_yes() { + let problem = issue_example_yes(); + let solver = BruteForce::new(); + let solution = solver + .find_satisfying(&problem) + .expect("should find a schedule"); + assert!(problem.evaluate(&solution)); + assert!(problem.total_weighted_tardiness(&solution).unwrap() <= problem.bound()); +} + +#[test] +fn test_sequencing_to_minimize_weighted_tardiness_solver_no() { + let problem = issue_example_no(); + let solver = BruteForce::new(); + assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_all_satisfying(&problem).is_empty()); +} + +#[test] +fn test_sequencing_to_minimize_weighted_tardiness_paper_example() { + let yes = issue_example_yes(); + let no = issue_example_no(); + let solver = BruteForce::new(); + let config = vec![0, 0, 2, 1, 0]; + + assert_eq!(yes.total_weighted_tardiness(&config), Some(13)); + assert!(yes.evaluate(&config)); + assert!(!no.evaluate(&config)); + + let satisfying = solver.find_all_satisfying(&yes); + assert_eq!(satisfying, vec![config]); + assert!(solver.find_all_satisfying(&no).is_empty()); +} + +#[test] +fn test_sequencing_to_minimize_weighted_tardiness_serialization() { + let problem = issue_example_yes(); + let json = serde_json::to_value(&problem).unwrap(); + let restored: SequencingToMinimizeWeightedTardiness = serde_json::from_value(json).unwrap(); + assert_eq!(restored.lengths(), problem.lengths()); + assert_eq!(restored.weights(), problem.weights()); + assert_eq!(restored.deadlines(), problem.deadlines()); + assert_eq!(restored.bound(), problem.bound()); +} diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index 4365bef31..86a4e4c23 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -138,6 +138,10 @@ fn test_all_problems_implement_trait_correctly() { &FlowShopScheduling::new(2, vec![vec![1, 2], vec![3, 4]], 10), "FlowShopScheduling", ); + check_problem_trait( + &SequencingToMinimizeWeightedTardiness::new(vec![3, 4, 2], vec![2, 3, 1], vec![5, 8, 4], 4), + "SequencingToMinimizeWeightedTardiness", + ); check_problem_trait( &MinimumTardinessSequencing::new(3, vec![2, 3, 1], vec![(0, 2)]), "MinimumTardinessSequencing", From e45d58999ebeb1047d7a0679421202dba10bfc15 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 22:05:17 +0800 Subject: [PATCH 3/5] chore: remove plan file after implementation --- ...quencing-to-minimize-weighted-tardiness.md | 270 ------------------ 1 file changed, 270 deletions(-) delete mode 100644 docs/plans/2026-03-16-sequencing-to-minimize-weighted-tardiness.md diff --git a/docs/plans/2026-03-16-sequencing-to-minimize-weighted-tardiness.md b/docs/plans/2026-03-16-sequencing-to-minimize-weighted-tardiness.md deleted file mode 100644 index 86928d88b..000000000 --- a/docs/plans/2026-03-16-sequencing-to-minimize-weighted-tardiness.md +++ /dev/null @@ -1,270 +0,0 @@ -# SequencingToMinimizeWeightedTardiness Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Add the `SequencingToMinimizeWeightedTardiness` model as a decision/satisfaction problem with brute-force-compatible permutation encoding, CLI creation support, canonical example data, tests, and paper documentation. - -**Architecture:** Implement a new `misc` model that follows the Lehmer-code permutation pattern already used by `FlowShopScheduling` and `MinimumTardinessSequencing`. The model stores `lengths`, `weights`, `deadlines`, and `bound`, decodes schedule permutations, computes total weighted tardiness for the induced single-machine schedule, and evaluates to `true` exactly when the permutation is valid and the total weighted tardiness is at most `bound`. Batch 1 covers code, registry, CLI, example-db, and tests. Batch 2 covers the Typst paper entry and bibliography, after Batch 1 makes the canonical example export available. - -**Tech Stack:** Rust workspace (`problemreductions` and `problemreductions-cli`), serde/inventory registry, brute-force solver, Typst paper, GitHub issue `#498`, companion rule issue `#473`. - ---- - -## Issue Notes - -- Treat this as the decision form from Garey & Johnson SS5: given lengths `l_j`, weights `w_j`, deadlines `d_j`, and bound `K`, ask whether some one-machine schedule has total weighted tardiness at most `K`. -- Reuse the issue's 5-task example data, but fix the bound inconsistency called out in the issue comments. -- Brute-force verification on the issue instance shows a unique optimal schedule `(t_1, t_2, t_5, t_4, t_3)` with total weighted tardiness `13`. -- Use `K = 13` for the canonical YES example and `K = 12` for the corresponding NO test. -- Associated rule issue already exists: `#473 [Rule] 3-Partition to Sequencing to Minimize Weighted Tardiness`, so this model is not orphaned. - -## Batch 1: Model, CLI, Example-DB, Tests - -### Task 1: Write the failing model tests first - -**Files:** -- Create: `src/unit_tests/models/misc/sequencing_to_minimize_weighted_tardiness.rs` -- Modify: `src/unit_tests/trait_consistency.rs` - -**Step 1: Write the failing test file** - -Cover the concrete behaviors below before any production code exists: -- construction/accessors/dims/problem metadata -- total weighted tardiness helper on the issue example -- YES evaluation for the optimal Lehmer code `[0, 0, 2, 1, 0]` with `K = 13` -- NO evaluation for the same schedule under `K = 12` -- invalid configs: wrong length and invalid Lehmer digits -- brute-force solver returns a satisfying schedule for `K = 13` and none for `K = 12` -- serde round-trip -- trait consistency registration entry - -**Step 2: Run the focused tests to verify RED** - -Run: `cargo test sequencing_to_minimize_weighted_tardiness --lib` - -Expected: compile failure because the new model module and test target do not exist yet. - -**Step 3: Add the trait-consistency failing assertions** - -Add: -- `check_problem_trait(&SequencingToMinimizeWeightedTardiness::new(...), "SequencingToMinimizeWeightedTardiness")` - -Run: `cargo test trait_consistency --lib` - -Expected: compile failure because the type is not defined or not re-exported yet. - -**Step 4: Commit the red state if the branch policy allows partial commits** - -```bash -git add src/unit_tests/models/misc/sequencing_to_minimize_weighted_tardiness.rs src/unit_tests/trait_consistency.rs -git commit -m "test: add failing tests for weighted tardiness sequencing" -``` - -### Task 2: Implement the new model and core wiring - -**Files:** -- Create: `src/models/misc/sequencing_to_minimize_weighted_tardiness.rs` -- Modify: `src/models/misc/mod.rs` -- Modify: `src/models/mod.rs` -- Modify: `src/lib.rs` - -**Step 1: Implement the model with the smallest code needed to satisfy Task 1** - -The new file should include: -- `ProblemSchemaEntry` with fields `lengths`, `weights`, `deadlines`, `bound` -- struct definition with serde derives -- constructor `new(lengths: Vec, weights: Vec, deadlines: Vec, bound: u64)` -- accessors `lengths()`, `weights()`, `deadlines()`, `bound()`, `num_tasks()` -- helper(s) to decode Lehmer code into job order -- helper `total_weighted_tardiness(&self, config: &[usize]) -> Option` or equivalent safe numeric return -- `Problem` impl with `type Metric = bool`, `dims() = [n, n-1, ..., 1]`, and `evaluate()` returning `true` iff the config is a valid Lehmer permutation with total weighted tardiness `<= bound` -- `SatisfactionProblem` impl -- `declare_variants! { default sat SequencingToMinimizeWeightedTardiness => "factorial(num_tasks)", }` -- `#[cfg(test)]` link to the new unit test file -- `#[cfg(feature = "example-db")]` canonical example spec using the issue instance and the optimal config `[0, 0, 2, 1, 0]` - -**Step 2: Wire the model into exports** - -Update module/re-export files so the type is visible through: -- `crate::models::misc::*` -- `crate::models::*` -- `crate::prelude::*` - -Also extend `src/models/misc/mod.rs` so the example-db aggregator includes the new canonical example spec. - -**Step 3: Run the focused tests to reach GREEN** - -Run: -- `cargo test sequencing_to_minimize_weighted_tardiness --lib` -- `cargo test trait_consistency --lib` - -Expected: the new tests and trait-consistency entries pass. - -**Step 4: Refactor only after green** - -If needed, extract small private helpers so the model matches the style of `FlowShopScheduling` and `MinimumTardinessSequencing`. Do not add solver features beyond what the tests require. - -**Step 5: Commit** - -```bash -git add src/models/misc/sequencing_to_minimize_weighted_tardiness.rs src/models/misc/mod.rs src/models/mod.rs src/lib.rs src/unit_tests/models/misc/sequencing_to_minimize_weighted_tardiness.rs src/unit_tests/trait_consistency.rs -git commit -m "feat: add sequencing to minimize weighted tardiness model" -``` - -### Task 3: Add CLI creation support and user-facing help - -**Files:** -- Modify: `problemreductions-cli/src/commands/create.rs` -- Modify: `problemreductions-cli/src/cli.rs` - -**Step 1: Write or extend the failing CLI-facing tests if nearby coverage exists** - -If there is an established CLI create test location for similar models, add a focused regression there. If not, keep this task implementation-focused and rely on targeted command execution for verification. - -**Step 2: Add the create-arm** - -Implement `pred create SequencingToMinimizeWeightedTardiness` using existing flags: -- `--sizes` for processing lengths -- `--weights` for tardiness weights -- `--deadlines` for job deadlines -- `--bound` for the decision threshold - -Requirements: -- parse all four inputs -- require equal vector lengths -- convert `--bound` from the existing integer flag type to `u64` with a clear error if negative -- instantiate `SequencingToMinimizeWeightedTardiness::new(...)` - -**Step 3: Update help text** - -Add the new problem to the `CreateArgs` after-help table with the exact flag combination above. No new CLI flags should be introduced for this model. - -**Step 4: Verify the CLI path** - -Run: -- `cargo test create --package problemreductions-cli` -- `cargo run -p problemreductions-cli -- create SequencingToMinimizeWeightedTardiness --sizes 3,4,2,5,3 --weights 2,3,1,4,2 --deadlines 5,8,4,15,10 --bound 13` - -Expected: -- tests stay green -- the command emits JSON for the new model with the expected fields - -**Step 5: Commit** - -```bash -git add problemreductions-cli/src/commands/create.rs problemreductions-cli/src/cli.rs -git commit -m "feat: add CLI creation for weighted tardiness sequencing" -``` - -### Task 4: Finalize Batch 1 verification - -**Files:** -- Review only: all files touched in Tasks 1-3 - -**Step 1: Run the model-level verification suite** - -Run: -- `cargo test sequencing_to_minimize_weighted_tardiness --lib` -- `cargo test trait_consistency --lib` -- `cargo test example_db --lib --features example-db` - -Expected: all pass. - -**Step 2: Run the repo quick check if Batch 1 is stable** - -Run: `make check` - -Expected: format, clippy, and test checks pass for the workspace. - -**Step 3: Note any generated-file changes** - -If verification regenerates tracked exports, inspect them and keep only expected changes related to the new model. - -## Batch 2: Paper Entry and Citation Wiring - -### Task 5: Add the paper entry and bibliography - -**Files:** -- Modify: `docs/paper/reductions.typ` -- Modify: `docs/paper/references.bib` -- Test: `src/unit_tests/models/misc/sequencing_to_minimize_weighted_tardiness.rs` - -**Step 1: Add bibliography entries** - -Add the citations needed by the paper text if they are missing: -- Garey & Johnson book entry already exists; reuse it -- Lawler 1977 reference for strong NP-hardness / special equal-weight case -- Potts & Van Wassenhove 1985 branch-and-bound -- Tanaka, Fujikuma, and Araki 2009 exact algorithm - -**Step 2: Register the display name** - -Add: - -```typst -"SequencingToMinimizeWeightedTardiness": [Sequencing to Minimize Weighted Tardiness], -``` - -**Step 3: Write the `problem-def(...)` entry** - -Place it in the scheduling section near `MinimumTardinessSequencing`. The entry should: -- define the decision problem with lengths, weights, deadlines, and bound `K` -- mention standard notation `1 || sum w_j T_j` -- explain strong NP-hardness and the key tractable special cases from the issue context -- use the canonical example data and the optimal order `(t_1, t_2, t_5, t_4, t_3)` -- show that the completion times are `(3, 7, 10, 15, 17)` and only `t_3` is tardy, contributing `13` -- make the example visually clear from the start instead of changing `K` midway - -**Step 4: Add the paper-example regression test** - -Extend `src/unit_tests/models/misc/sequencing_to_minimize_weighted_tardiness.rs` with a `test_sequencing_to_minimize_weighted_tardiness_paper_example` that: -- constructs the exact paper instance -- asserts the canonical config `[0, 0, 2, 1, 0]` is satisfying for `K = 13` -- asserts the same ordering is not satisfying for `K = 12` -- uses brute force to confirm the YES instance has at least one satisfying schedule and the NO instance has none - -**Step 5: Verify the paper batch** - -Run: -- `cargo test sequencing_to_minimize_weighted_tardiness --lib` -- `make paper` - -Expected: tests pass and the Typst paper builds successfully. - -**Step 6: Commit** - -```bash -git add docs/paper/reductions.typ docs/paper/references.bib src/unit_tests/models/misc/sequencing_to_minimize_weighted_tardiness.rs -git commit -m "docs: add paper entry for weighted tardiness sequencing" -``` - -## Final Verification and Review Handoff - -### Task 6: Full repo verification and review preparation - -**Files:** -- Review only: all touched files - -**Step 1: Run final verification** - -Run: -- `make check` -- `make paper` - -If runtime is acceptable, also run: -- `make test` - -**Step 2: Run implementation review** - -Invoke the repo-local review skill after code is complete: -- `review-implementation` - -Fix structural or Important quality findings before the implementation summary is posted to the PR. - -**Step 3: Prepare the PR summary** - -Call out: -- decision-model choice (not optimization-model choice) -- corrected canonical example (`K = 13` YES, `K = 12` NO) -- CLI flags used for construction -- any deviations from this plan From 8c7a3c9f2abab9659d8d8fb46628005921ba6d3e Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Fri, 20 Mar 2026 02:16:31 +0800 Subject: [PATCH 4/5] Fix merge-with-main issues: update example_db API, fix paper Typst blocks, format - Update canonical_model_example_specs to use new ModelExampleSpec struct fields - Fix duplicate #{ block opener in reductions.typ from merge - Restore missing load-model-example and prefix-sums bindings for CumulativeCost - Run cargo fmt to fix import line wrapping Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/paper/reductions.typ | 6 +++--- problemreductions-cli/src/commands/create.rs | 4 ++-- ...sequencing_to_minimize_weighted_tardiness.rs | 17 ++++++++--------- src/models/mod.rs | 5 ++--- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index f2c177fb3..09da8adb3 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -3519,7 +3519,6 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], ] } -#{ #{ let x = load-model-example("SequencingToMinimizeWeightedTardiness") let lengths = x.instance.lengths @@ -3527,8 +3526,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let deadlines = x.instance.deadlines let bound = x.instance.bound let njobs = lengths.len() - let sol = x.optimal.at(0) - let lehmer = sol.config + let lehmer = x.optimal_config let schedule = { let avail = range(njobs) let result = () @@ -3602,6 +3600,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], } #{ + let x = load-model-example("SequencingToMinimizeMaximumCumulativeCost") let costs = x.instance.costs let precs = x.instance.precedences let bound = x.instance.bound @@ -3616,6 +3615,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], } result } + let prefix-sums = { let running = 0 let result = () for task in schedule { diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 5b48c1232..ffa41178c 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -19,8 +19,8 @@ use problemreductions::models::misc::{ MultiprocessorScheduling, PaintShop, PartiallyOrderedKnapsack, QueryArg, RectilinearPictureCompression, ResourceConstrainedScheduling, SequencingToMinimizeMaximumCumulativeCost, SequencingToMinimizeWeightedTardiness, - SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, - ShortestCommonSupersequence, StringToStringCorrection, SubsetSum, SumOfSquaresPartition, + SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, ShortestCommonSupersequence, + StringToStringCorrection, SubsetSum, SumOfSquaresPartition, }; use problemreductions::models::BiconnectivityAugmentation; use problemreductions::prelude::*; diff --git a/src/models/misc/sequencing_to_minimize_weighted_tardiness.rs b/src/models/misc/sequencing_to_minimize_weighted_tardiness.rs index 75fc40072..58789cdfe 100644 --- a/src/models/misc/sequencing_to_minimize_weighted_tardiness.rs +++ b/src/models/misc/sequencing_to_minimize_weighted_tardiness.rs @@ -180,15 +180,14 @@ crate::declare_variants! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "sequencing_to_minimize_weighted_tardiness", - build: || { - let problem = SequencingToMinimizeWeightedTardiness::new( - vec![3, 4, 2, 5, 3], - vec![2, 3, 1, 4, 2], - vec![5, 8, 4, 15, 10], - 13, - ); - crate::example_db::specs::satisfaction_example(problem, vec![vec![0, 0, 2, 1, 0]]) - }, + instance: Box::new(SequencingToMinimizeWeightedTardiness::new( + vec![3, 4, 2, 5, 3], + vec![2, 3, 1, 4, 2], + vec![5, 8, 4, 15, 10], + 13, + )), + optimal_config: vec![0, 0, 2, 1, 0], + optimal_value: serde_json::json!(true), }] } diff --git a/src/models/mod.rs b/src/models/mod.rs index 6b7fe5ab8..7e4b11e91 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -32,9 +32,8 @@ pub use misc::{ MultiprocessorScheduling, PaintShop, Partition, PrecedenceConstrainedScheduling, QueryArg, RectilinearPictureCompression, ResourceConstrainedScheduling, SequencingToMinimizeMaximumCumulativeCost, SequencingToMinimizeWeightedTardiness, - SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, - ShortestCommonSupersequence, StaffScheduling, StringToStringCorrection, SubsetSum, - SumOfSquaresPartition, Term, + SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, ShortestCommonSupersequence, + StaffScheduling, StringToStringCorrection, SubsetSum, SumOfSquaresPartition, Term, }; pub use set::{ ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking, From 819005f0c44f3a0c3f2d95b492e95b97b053f0e3 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Fri, 20 Mar 2026 02:23:10 +0800 Subject: [PATCH 5/5] Fix CLI flag mismatch: map schema field 'lengths' to --sizes The auto-generated Parameters section showed --lengths (from the schema field name) while the Example and actual handler used --sizes. Add a field-name override so both are consistent. Co-Authored-By: Claude Opus 4.6 (1M context) --- problemreductions-cli/src/commands/create.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index ffa41178c..132f0e5af 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -496,6 +496,7 @@ fn help_flag_name(canonical: &str, field_name: &str) -> String { "num_tasks" => "n".to_string(), "precedences" => "precedence-pairs".to_string(), "threshold" => "bound".to_string(), + "lengths" => "sizes".to_string(), _ => field_name.replace('_', "-"), } }