diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index b14ef6ea..743d2075 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 53e868b8..a5dee411 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 783651a3..03d77b8d 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 8488ef01..bbf8c0f5 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 919dffd3..1e905d09 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 cf330a93..90e91cdc 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 3855045f..7de25f94 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 b3f46deb..e0aebae9 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 2b5c4f4b..340512b1 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 cc96aa83..af5037cd 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 5395038f..7ed647c5 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 00000000..ca12d961 --- /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 4365bef3..86a4e4c2 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",