diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 65a15f1d1..f50c5ee53 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -96,6 +96,7 @@ "SubgraphIsomorphism": [Subgraph Isomorphism], "PartitionIntoTriangles": [Partition Into Triangles], "FlowShopScheduling": [Flow Shop Scheduling], + "MinimumTardinessSequencing": [Minimum Tardiness Sequencing], ) // Definition label: "def:" — each definition block must have a matching label @@ -1781,6 +1782,82 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS ) ] +#{ + let x = load-model-example("MinimumTardinessSequencing") + let ntasks = x.instance.num_tasks + let deadlines = x.instance.deadlines + let precs = x.instance.precedences + let sol = x.optimal.at(0) + let tardy-count = sol.metric.Valid + // Decode Lehmer code to permutation (schedule order) + let lehmer = sol.config + let schedule = { + let avail = range(ntasks) + let result = () + for c in lehmer { + result.push(avail.at(c)) + avail = avail.enumerate().filter(((i, v)) => i != c).map(((i, v)) => v) + } + result + } + // Compute inverse: task-pos[task] = position + let task-pos = range(ntasks).map(task => { + schedule.enumerate().filter(((p, t)) => t == task).at(0).at(0) + }) + // Identify tardy tasks + let tardy-tasks = range(ntasks).filter(t => task-pos.at(t) + 1 > deadlines.at(t)) + [ + #problem-def("MinimumTardinessSequencing")[ + Given a set $T$ of $n$ unit-length tasks, a deadline function $d: T -> ZZ^+$, and a partial order $prec.eq$ on $T$, find a one-machine schedule $sigma: T -> {1, 2, dots, n}$ that respects the precedence constraints (if $t_i prec.eq t_j$ then $sigma(t_i) < sigma(t_j)$) and minimizes the number of _tardy_ tasks, i.e., tasks $t$ with $sigma(t) > d(t)$. + ][ + Minimum Tardiness Sequencing is a classical NP-complete scheduling problem catalogued as SS2 in Garey & Johnson @garey1979. In standard scheduling notation it is written $1 | "prec", p_j = 1 | sum U_j$, where $U_j = 1$ if job $j$ finishes after its deadline and $U_j = 0$ otherwise. + + The problem is NP-complete by reduction from Clique (Theorem 3.10 in @garey1979). When the precedence constraints are empty, the problem becomes solvable in $O(n log n)$ time by Moore's algorithm @moore1968: sort tasks by deadline and greedily schedule each task on time, removing the task with the largest processing time whenever a deadline violation occurs. With arbitrary precedence constraints and unit processing times, the problem remains strongly NP-hard. + + *Example.* Consider $n = #ntasks$ tasks with deadlines $d = (#deadlines.map(v => str(v)).join(", "))$ and precedence constraint #{precs.map(p => [$t_#(p.at(0)) prec.eq t_#(p.at(1))$]).join(", ")}. An optimal schedule places tasks in order $(#schedule.map(t => $t_#t$).join(", "))$, giving #tardy-count tardy #if tardy-count == 1 [task] else [tasks]#{if tardy-tasks.len() > 0 [ ($#{tardy-tasks.map(t => $t_#t$).join(", ")}$ #if tardy-tasks.len() == 1 [finishes] else [finish] after #if tardy-tasks.len() == 1 [its deadline] else [their deadlines])]}. + + #figure( + canvas(length: 1cm, { + import draw: * + let colors = (rgb("#4e79a7"), rgb("#e15759"), rgb("#76b7b2"), rgb("#f28e2b"), rgb("#59a14f")) + let scale = 1.2 + let row-h = 0.6 + + // Draw schedule blocks (single machine, unit-length tasks) + for (pos, task) in schedule.enumerate() { + let x0 = pos * scale + let x1 = (pos + 1) * scale + let is-tardy = tardy-tasks.contains(task) + let fill = colors.at(calc.rem(task, colors.len())).transparentize(if is-tardy { 70% } else { 30% }) + let stroke-color = colors.at(calc.rem(task, colors.len())) + rect((x0, -row-h / 2), (x1, row-h / 2), + fill: fill, stroke: 0.4pt + stroke-color) + content(((x0 + x1) / 2, 0), text(7pt, $t_#task$)) + // Deadline marker for this task + let dl = deadlines.at(task) + if dl <= ntasks { + let dl-x = dl * scale + line((dl-x, row-h / 2 + 0.05 + task * 0.12), (dl-x, row-h / 2 + 0.15 + task * 0.12), + stroke: (paint: if is-tardy { red } else { green.darken(20%) }, thickness: 0.6pt)) + } + } + + // Time axis + let y-axis = -row-h / 2 - 0.2 + line((0, y-axis), (ntasks * scale, y-axis), stroke: 0.4pt) + for t in range(ntasks + 1) { + let x = t * scale + line((x, y-axis), (x, y-axis - 0.1), stroke: 0.4pt) + content((x, y-axis - 0.25), text(6pt, str(t + 1))) + } + content((ntasks * scale / 2, y-axis - 0.45), text(7pt)[finish time]) + }), + caption: [Optimal schedule for #ntasks tasks. #if tardy-tasks.len() > 0 [Faded #if tardy-tasks.len() == 1 [block indicates the] else [blocks indicate] tardy #if tardy-tasks.len() == 1 [task] else [tasks] (finish time exceeds deadline).] else [All tasks meet their deadlines.]], + ) + ] + ] +} + // 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 64bd0b76b..5fc78c6e0 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -8,6 +8,17 @@ @article{juttner2018 doi = {10.1016/j.dam.2018.02.018} } +@article{moore1968, + author = {J. Michael Moore}, + title = {An $n$ Job, One Machine Sequencing Algorithm for Minimizing the Number of Late Jobs}, + journal = {Management Science}, + volume = {15}, + number = {1}, + pages = {102--109}, + year = {1968}, + doi = {10.1287/mnsc.15.1.102} +} + @article{johnson1954, author = {Selmer M. Johnson}, title = {Optimal two- and three-stage production schedules with setup times included}, diff --git a/docs/src/cli.md b/docs/src/cli.md index 9cb810db0..6507c61f5 100644 --- a/docs/src/cli.md +++ b/docs/src/cli.md @@ -275,6 +275,7 @@ pred create MaxCut --graph 0-1,1-2,2-0 -o maxcut.json pred create Factoring --target 15 --bits-m 4 --bits-n 4 -o factoring.json pred create Factoring --target 21 --bits-m 3 --bits-n 3 -o factoring2.json pred create X3C --universe 9 --sets "0,1,2;0,2,4;3,4,5;3,5,7;6,7,8;1,4,6;2,5,8" -o x3c.json +pred create MinimumTardinessSequencing --n 5 --deadlines 5,5,5,3,3 --precedence-pairs "0>3,1>3,1>4,2>4" -o mts.json ``` Canonical examples are useful when you want a known-good instance from the paper/example database. diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index 9e3861c70..3a6e9df87 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -461,6 +461,27 @@ } ] }, + { + "name": "MinimumTardinessSequencing", + "description": "Schedule unit-length tasks with precedence constraints and deadlines to minimize the number of tardy tasks", + "fields": [ + { + "name": "num_tasks", + "type_name": "usize", + "description": "Number of tasks |T|" + }, + { + "name": "deadlines", + "type_name": "Vec", + "description": "Deadline d(t) for each task (1-indexed finish time)" + }, + { + "name": "precedences", + "type_name": "Vec<(usize, usize)>", + "description": "Precedence pairs (predecessor, successor)" + } + ] + }, { "name": "MinimumVertexCover", "description": "Find minimum weight vertex cover in a graph", diff --git a/docs/src/reductions/reduction_graph.json b/docs/src/reductions/reduction_graph.json index 6b5a24dee..c1334cfb2 100644 --- a/docs/src/reductions/reduction_graph.json +++ b/docs/src/reductions/reduction_graph.json @@ -396,6 +396,13 @@ "doc_path": "models/graph/struct.MinimumSumMulticenter.html", "complexity": "2^num_vertices" }, + { + "name": "MinimumTardinessSequencing", + "variant": {}, + "category": "misc", + "doc_path": "models/misc/struct.MinimumTardinessSequencing.html", + "complexity": "2^num_tasks" + }, { "name": "MinimumVertexCover", "variant": { @@ -542,7 +549,7 @@ }, { "source": 4, - "target": 53, + "target": 54, "overhead": [ { "field": "num_spins", @@ -602,7 +609,7 @@ }, { "source": 12, - "target": 48, + "target": 49, "overhead": [ { "field": "num_vars", @@ -643,7 +650,7 @@ }, { "source": 19, - "target": 48, + "target": 49, "overhead": [ { "field": "num_vars", @@ -669,7 +676,7 @@ }, { "source": 20, - "target": 48, + "target": 49, "overhead": [ { "field": "num_vars", @@ -695,7 +702,7 @@ }, { "source": 21, - "target": 48, + "target": 49, "overhead": [ { "field": "num_vars", @@ -706,7 +713,7 @@ }, { "source": 21, - "target": 55, + "target": 56, "overhead": [ { "field": "num_elements", @@ -717,7 +724,7 @@ }, { "source": 22, - "target": 50, + "target": 51, "overhead": [ { "field": "num_clauses", @@ -736,7 +743,7 @@ }, { "source": 23, - "target": 48, + "target": 49, "overhead": [ { "field": "num_vars", @@ -762,7 +769,7 @@ }, { "source": 25, - "target": 53, + "target": 54, "overhead": [ { "field": "num_spins", @@ -942,7 +949,7 @@ }, { "source": 31, - "target": 44, + "target": 45, "overhead": [ { "field": "num_vertices", @@ -1077,7 +1084,7 @@ }, { "source": 37, - "target": 48, + "target": 49, "overhead": [ { "field": "num_vars", @@ -1162,7 +1169,7 @@ "doc_path": "rules/minimumsetcovering_ilp/index.html" }, { - "source": 44, + "source": 45, "target": 31, "overhead": [ { @@ -1177,7 +1184,7 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 44, + "source": 45, "target": 42, "overhead": [ { @@ -1192,7 +1199,7 @@ "doc_path": "rules/minimumvertexcover_minimumsetcovering/index.html" }, { - "source": 48, + "source": 49, "target": 12, "overhead": [ { @@ -1207,8 +1214,8 @@ "doc_path": "rules/qubo_ilp/index.html" }, { - "source": 48, - "target": 52, + "source": 49, + "target": 53, "overhead": [ { "field": "num_spins", @@ -1218,7 +1225,7 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 50, + "source": 51, "target": 4, "overhead": [ { @@ -1233,7 +1240,7 @@ "doc_path": "rules/sat_circuitsat/index.html" }, { - "source": 50, + "source": 51, "target": 16, "overhead": [ { @@ -1248,7 +1255,7 @@ "doc_path": "rules/sat_coloring/index.html" }, { - "source": 50, + "source": 51, "target": 21, "overhead": [ { @@ -1263,7 +1270,7 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 50, + "source": 51, "target": 30, "overhead": [ { @@ -1278,7 +1285,7 @@ "doc_path": "rules/sat_maximumindependentset/index.html" }, { - "source": 50, + "source": 51, "target": 39, "overhead": [ { @@ -1293,8 +1300,8 @@ "doc_path": "rules/sat_minimumdominatingset/index.html" }, { - "source": 52, - "target": 48, + "source": 53, + "target": 49, "overhead": [ { "field": "num_vars", @@ -1304,7 +1311,7 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 53, + "source": 54, "target": 25, "overhead": [ { @@ -1319,8 +1326,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 53, - "target": 52, + "source": 54, + "target": 53, "overhead": [ { "field": "num_spins", @@ -1334,7 +1341,7 @@ "doc_path": "rules/spinglass_casts/index.html" }, { - "source": 56, + "source": 57, "target": 12, "overhead": [ { @@ -1349,8 +1356,8 @@ "doc_path": "rules/travelingsalesman_ilp/index.html" }, { - "source": 56, - "target": 48, + "source": 57, + "target": 49, "overhead": [ { "field": "num_vars", diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 666768780..c42f69c24 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -241,6 +241,7 @@ Flags by problem type: FAS --arcs [--weights] [--num-vertices] FVS --arcs [--weights] [--num-vertices] FlowShopScheduling --task-lengths, --deadline [--num-processors] + MinimumTardinessSequencing --n, --deadlines [--precedence-pairs] SCS --strings, --bound [--alphabet-size] ILP, CircuitSAT (via reduction only) @@ -384,6 +385,12 @@ pub struct CreateArgs { /// Directed arcs for directed graph problems (e.g., 0>1,1>2,2>0) #[arg(long)] pub arcs: Option, + /// Deadlines for MinimumTardinessSequencing (comma-separated, e.g., "5,5,5,3,3") + #[arg(long)] + pub deadlines: Option, + /// Precedence pairs for MinimumTardinessSequencing (e.g., "0>3,1>3,1>4,2>4") + #[arg(long)] + pub precedence_pairs: Option, /// Task lengths for FlowShopScheduling (semicolon-separated rows: "3,4,2;2,3,5;4,1,3") #[arg(long)] pub task_lengths: Option, diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index be249c836..a14442240 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -8,8 +8,8 @@ use problemreductions::export::{ModelExample, ProblemRef, ProblemSide, RuleExamp use problemreductions::models::algebraic::{ClosestVectorProblem, BMF}; use problemreductions::models::graph::{GraphPartitioning, HamiltonianPath}; use problemreductions::models::misc::{ - BinPacking, FlowShopScheduling, LongestCommonSubsequence, PaintShop, - ShortestCommonSupersequence, SubsetSum, + BinPacking, FlowShopScheduling, LongestCommonSubsequence, MinimumTardinessSequencing, + PaintShop, ShortestCommonSupersequence, SubsetSum, }; use problemreductions::prelude::*; use problemreductions::registry::collect_schemas; @@ -57,6 +57,8 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.pattern.is_none() && args.strings.is_none() && args.arcs.is_none() + && args.deadlines.is_none() + && args.precedence_pairs.is_none() && args.task_lengths.is_none() && args.deadline.is_none() && args.num_processors.is_none() @@ -788,6 +790,64 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // MinimumTardinessSequencing + "MinimumTardinessSequencing" => { + let deadlines_str = args.deadlines.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "MinimumTardinessSequencing requires --deadlines and --n\n\n\ + Usage: pred create MinimumTardinessSequencing --n 5 --deadlines 5,5,5,3,3 [--precedence-pairs \"0>3,1>3,1>4,2>4\"]" + ) + })?; + let num_tasks = args.n.ok_or_else(|| { + anyhow::anyhow!( + "MinimumTardinessSequencing requires --n (number of tasks)\n\n\ + Usage: pred create MinimumTardinessSequencing --n 5 --deadlines 5,5,5,3,3" + ) + })?; + let deadlines: Vec = util::parse_comma_list(deadlines_str)?; + let precedences: Vec<(usize, usize)> = match args.precedence_pairs.as_deref() { + Some(s) if !s.is_empty() => s + .split(',') + .map(|pair| { + let parts: Vec<&str> = pair.trim().split('>').collect(); + anyhow::ensure!( + parts.len() == 2, + "Invalid precedence format '{}', expected 'u>v'", + pair.trim() + ); + Ok(( + parts[0].trim().parse::()?, + parts[1].trim().parse::()?, + )) + }) + .collect::>>()?, + _ => vec![], + }; + anyhow::ensure!( + deadlines.len() == num_tasks, + "deadlines length ({}) must equal num_tasks ({})", + deadlines.len(), + num_tasks + ); + for &(pred, succ) in &precedences { + anyhow::ensure!( + pred < num_tasks && succ < num_tasks, + "precedence index out of range: ({}, {}) but num_tasks = {}", + pred, + succ, + num_tasks + ); + } + ( + ser(MinimumTardinessSequencing::new( + num_tasks, + deadlines, + precedences, + ))?, + resolved_variant.clone(), + ) + } + // OptimalLinearArrangement — graph + bound "OptimalLinearArrangement" => { let (graph, _) = parse_graph(args).map_err(|e| { diff --git a/src/lib.rs b/src/lib.rs index 5ed9a1c44..bceccfe25 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,8 +55,8 @@ pub mod prelude { PartitionIntoTriangles, RuralPostman, TravelingSalesman, }; pub use crate::models::misc::{ - BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, PaintShop, - ShortestCommonSupersequence, SubsetSum, + BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, + MinimumTardinessSequencing, PaintShop, ShortestCommonSupersequence, SubsetSum, }; pub use crate::models::set::{ExactCoverBy3Sets, MaximumSetPacking, MinimumSetCovering}; diff --git a/src/models/misc/minimum_tardiness_sequencing.rs b/src/models/misc/minimum_tardiness_sequencing.rs new file mode 100644 index 000000000..ed383c816 --- /dev/null +++ b/src/models/misc/minimum_tardiness_sequencing.rs @@ -0,0 +1,213 @@ +//! Minimum Tardiness Sequencing problem implementation. +//! +//! A classical NP-complete single-machine scheduling problem (SS2 from +//! Garey & Johnson, 1979) where unit-length tasks with precedence constraints +//! and deadlines must be scheduled to minimize the number of tardy tasks. +//! Corresponds to scheduling notation `1|prec, pj=1|sum Uj`. + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::traits::{OptimizationProblem, Problem}; +use crate::types::{Direction, SolutionSize}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "MinimumTardinessSequencing", + display_name: "Minimum Tardiness Sequencing", + aliases: &[], + dimensions: &[], + module_path: module_path!(), + description: "Schedule unit-length tasks with precedence constraints and deadlines to minimize the number of tardy tasks", + fields: &[ + FieldInfo { name: "num_tasks", type_name: "usize", description: "Number of tasks |T|" }, + FieldInfo { name: "deadlines", type_name: "Vec", description: "Deadline d(t) for each task (1-indexed finish time)" }, + FieldInfo { name: "precedences", type_name: "Vec<(usize, usize)>", description: "Precedence pairs (predecessor, successor)" }, + ], + } +} + +/// Minimum Tardiness Sequencing problem. +/// +/// Given a set T of tasks, each with unit length and a deadline d(t), +/// and a partial order (precedence constraints) on T, find a schedule +/// `sigma: T -> {0, 1, ..., |T|-1}` that is a valid permutation, +/// respects precedence constraints (`sigma(t) < sigma(t')` whenever `t < t'`), +/// and minimizes the number of tardy tasks (`|{t : sigma(t)+1 > d(t)}|`). +/// +/// # Representation +/// +/// Each task has a variable representing its position in the schedule. +/// A configuration is valid if and only if it is a bijective mapping +/// (permutation) that respects all precedence constraints. +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::misc::MinimumTardinessSequencing; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// let problem = MinimumTardinessSequencing::new( +/// 3, +/// vec![2, 3, 1], +/// vec![(0, 2)], // task 0 must precede task 2 +/// ); +/// let solver = BruteForce::new(); +/// let solution = solver.find_best(&problem); +/// assert!(solution.is_some()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MinimumTardinessSequencing { + num_tasks: usize, + deadlines: Vec, + precedences: Vec<(usize, usize)>, +} + +impl MinimumTardinessSequencing { + /// Create a new MinimumTardinessSequencing instance. + /// + /// # Arguments + /// + /// * `num_tasks` - Number of tasks. + /// * `deadlines` - Deadline for each task (1-indexed: a task at position `p` finishes at time `p+1`). + /// * `precedences` - List of `(predecessor, successor)` pairs. + /// + /// # Panics + /// + /// Panics if `deadlines.len() != num_tasks` or if any task index in `precedences` + /// is out of range. + pub fn new(num_tasks: usize, deadlines: Vec, precedences: Vec<(usize, usize)>) -> Self { + assert_eq!( + deadlines.len(), + num_tasks, + "deadlines length must equal num_tasks" + ); + for &(pred, succ) in &precedences { + assert!( + pred < num_tasks, + "predecessor index {} out of range (num_tasks = {})", + pred, + num_tasks + ); + assert!( + succ < num_tasks, + "successor index {} out of range (num_tasks = {})", + succ, + num_tasks + ); + } + Self { + num_tasks, + deadlines, + precedences, + } + } + + /// Returns the number of tasks. + pub fn num_tasks(&self) -> usize { + self.num_tasks + } + + /// Returns the deadlines. + pub fn deadlines(&self) -> &[usize] { + &self.deadlines + } + + /// Returns the precedence constraints. + pub fn precedences(&self) -> &[(usize, usize)] { + &self.precedences + } + + /// Returns the number of precedence constraints. + pub fn num_precedences(&self) -> usize { + self.precedences.len() + } +} + +impl Problem for MinimumTardinessSequencing { + const NAME: &'static str = "MinimumTardinessSequencing"; + type Metric = SolutionSize; + + 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]) -> SolutionSize { + let n = self.num_tasks; + if config.len() != n { + return SolutionSize::Invalid; + } + + // Decode Lehmer code into a permutation. + // config[i] must be < n - i (the domain size for position i). + let mut available: Vec = (0..n).collect(); + let mut schedule = Vec::with_capacity(n); + for &c in config.iter() { + if c >= available.len() { + return SolutionSize::Invalid; + } + schedule.push(available.remove(c)); + } + + // schedule[i] = the task scheduled at position i. + // We need sigma(task) = position, i.e., the inverse permutation. + let mut sigma = vec![0usize; n]; + for (pos, &task) in schedule.iter().enumerate() { + sigma[task] = pos; + } + + // Check precedence constraints: for each (pred, succ), sigma(pred) < sigma(succ) + for &(pred, succ) in &self.precedences { + if sigma[pred] >= sigma[succ] { + return SolutionSize::Invalid; + } + } + + // Count tardy tasks: task t is tardy if sigma(t) + 1 > d(t) + let tardy_count = sigma + .iter() + .enumerate() + .filter(|&(t, &pos)| pos + 1 > self.deadlines[t]) + .count(); + + SolutionSize::Valid(tardy_count) + } +} + +impl OptimizationProblem for MinimumTardinessSequencing { + type Value = usize; + + fn direction(&self) -> Direction { + Direction::Minimize + } +} + +crate::declare_variants! { + default opt MinimumTardinessSequencing => "2^num_tasks", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "minimum_tardiness_sequencing", + build: || { + // 4 tasks with precedence 0 -> 2 (task 0 before task 2). + // Deadlines: task 0 by time 2, task 1 by time 3, task 2 by time 1, task 3 by time 4. + let problem = MinimumTardinessSequencing::new( + 4, + vec![2, 3, 1, 4], + vec![(0, 2)], + ); + // Sample config: Lehmer code [0,0,0,0] = identity permutation (schedule order 0,1,2,3) + crate::example_db::specs::optimization_example(problem, vec![vec![0, 0, 0, 0]]) + }, + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/minimum_tardiness_sequencing.rs"] +mod tests; diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index 86b71e759..cc96aa83e 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -6,6 +6,7 @@ //! - [`FlowShopScheduling`]: Flow Shop Scheduling (meet deadline on m processors) //! - [`Knapsack`]: 0-1 Knapsack (maximize value subject to weight capacity) //! - [`LongestCommonSubsequence`]: Longest Common Subsequence +//! - [`MinimumTardinessSequencing`]: Minimize tardy tasks in single-machine scheduling //! - [`PaintShop`]: Minimize color switches in paint shop scheduling //! - [`ShortestCommonSupersequence`]: Find a common supersequence of bounded length //! - [`SubsetSum`]: Find a subset summing to exactly a target value @@ -15,6 +16,7 @@ pub(crate) mod factoring; mod flow_shop_scheduling; mod knapsack; mod longest_common_subsequence; +mod minimum_tardiness_sequencing; pub(crate) mod paintshop; pub(crate) mod shortest_common_supersequence; mod subset_sum; @@ -24,6 +26,7 @@ pub use factoring::Factoring; pub use flow_shop_scheduling::FlowShopScheduling; pub use knapsack::Knapsack; pub use longest_common_subsequence::LongestCommonSubsequence; +pub use minimum_tardiness_sequencing::MinimumTardinessSequencing; pub use paintshop::PaintShop; pub use shortest_common_supersequence::ShortestCommonSupersequence; pub use subset_sum::SubsetSum; @@ -34,5 +37,6 @@ pub(crate) fn canonical_model_example_specs() -> Vec::NAME, + "MinimumTardinessSequencing" + ); + assert_eq!(::variant(), vec![]); +} + +#[test] +fn test_minimum_tardiness_sequencing_evaluate_optimal() { + // Example from issue: 5 tasks, optimal has 1 tardy task + let problem = MinimumTardinessSequencing::new( + 5, + vec![5, 5, 5, 3, 3], + vec![(0, 3), (1, 3), (1, 4), (2, 4)], + ); + // Lehmer code [0,0,1,0,0] decodes to schedule [0,1,3,2,4]: + // available=[0,1,2,3,4] pick idx 0 -> 0; available=[1,2,3,4] pick idx 0 -> 1; + // available=[2,3,4] pick idx 1 -> 3; available=[2,4] pick idx 0 -> 2; available=[4] pick idx 0 -> 4. + // sigma: task 0 at pos 0, task 1 at pos 1, task 3 at pos 2, task 2 at pos 3, task 4 at pos 4. + // t0 finishes at 1 <= 5, t1 at 2 <= 5, t3 at 3 <= 3, t2 at 4 <= 5, t4 at 5 > 3 (tardy) + let config = vec![0, 0, 1, 0, 0]; + assert_eq!(problem.evaluate(&config), SolutionSize::Valid(1)); +} + +#[test] +fn test_minimum_tardiness_sequencing_evaluate_invalid_lehmer() { + let problem = MinimumTardinessSequencing::new(3, vec![2, 3, 1], vec![]); + // dims = [3, 2, 1]; config [0, 2, 0] has 2 >= 2 (second dim), invalid Lehmer code + assert_eq!(problem.evaluate(&[0, 2, 0]), SolutionSize::Invalid); +} + +#[test] +fn test_minimum_tardiness_sequencing_evaluate_out_of_range() { + let problem = MinimumTardinessSequencing::new(3, vec![2, 3, 1], vec![]); + // dims = [3, 2, 1]; config [0, 1, 5] has 5 >= 1 (third dim), out of range + assert_eq!(problem.evaluate(&[0, 1, 5]), SolutionSize::Invalid); +} + +#[test] +fn test_minimum_tardiness_sequencing_evaluate_wrong_length() { + let problem = MinimumTardinessSequencing::new(3, vec![2, 3, 1], vec![]); + assert_eq!(problem.evaluate(&[0, 1]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 1, 2, 3]), SolutionSize::Invalid); +} + +#[test] +fn test_minimum_tardiness_sequencing_evaluate_precedence_violation() { + let problem = MinimumTardinessSequencing::new( + 3, + vec![3, 3, 3], + vec![(0, 1)], // task 0 must precede task 1 + ); + // Lehmer [0,0,0] -> schedule [0,1,2] -> sigma [0,1,2]: sigma(0)=0 < sigma(1)=1, valid + assert_eq!(problem.evaluate(&[0, 0, 0]), SolutionSize::Valid(0)); + // Lehmer [1,0,0] -> schedule [1,0,2] -> sigma [1,0,2]: sigma(0)=1 >= sigma(1)=0, violates + assert_eq!(problem.evaluate(&[1, 0, 0]), SolutionSize::Invalid); + // Lehmer [2,1,0] -> schedule [2,1,0] -> sigma [2,1,0]: sigma(0)=2 >= sigma(1)=1, violates + assert_eq!(problem.evaluate(&[2, 1, 0]), SolutionSize::Invalid); +} + +#[test] +fn test_minimum_tardiness_sequencing_evaluate_all_on_time() { + let problem = MinimumTardinessSequencing::new(3, vec![3, 3, 3], vec![]); + // All deadlines are 3, so any permutation of 3 tasks is on time + // Lehmer [0,0,0] -> schedule [0,1,2] + assert_eq!(problem.evaluate(&[0, 0, 0]), SolutionSize::Valid(0)); + // Lehmer [2,1,0] -> schedule [2,1,0] + assert_eq!(problem.evaluate(&[2, 1, 0]), SolutionSize::Valid(0)); +} + +#[test] +fn test_minimum_tardiness_sequencing_evaluate_all_tardy() { + // Deadlines are all 0 (impossible to meet since earliest finish is 1) + // Wait: deadlines are usize and d(t)=0 means finish must be <= 0, but finish is at least 1 + // Actually, let's use deadlines that can't be met + let problem = MinimumTardinessSequencing::new(2, vec![0, 0], vec![]); + // Lehmer [0,0] -> schedule [0,1] -> sigma [0,1] + // pos 0 finishes at 1 > 0 (tardy), pos 1 finishes at 2 > 0 (tardy) + assert_eq!(problem.evaluate(&[0, 0]), SolutionSize::Valid(2)); +} + +#[test] +fn test_minimum_tardiness_sequencing_brute_force() { + let problem = MinimumTardinessSequencing::new( + 5, + vec![5, 5, 5, 3, 3], + vec![(0, 3), (1, 3), (1, 4), (2, 4)], + ); + let solver = BruteForce::new(); + let solution = solver.find_best(&problem).expect("should find a solution"); + let metric = problem.evaluate(&solution); + // Optimal is 1 tardy task + assert_eq!(metric, SolutionSize::Valid(1)); +} + +#[test] +fn test_minimum_tardiness_sequencing_brute_force_no_precedences() { + // Without precedences, Moore's algorithm gives optimal + // 3 tasks: deadlines 1, 3, 2. Best is to schedule task with deadline 1 first. + let problem = MinimumTardinessSequencing::new(3, vec![1, 3, 2], vec![]); + let solver = BruteForce::new(); + let solution = solver.find_best(&problem).expect("should find a solution"); + let metric = problem.evaluate(&solution); + // All can be on time: t0 at pos 0 (finish 1 <= 1), t2 at pos 1 (finish 2 <= 2), t1 at pos 2 (finish 3 <= 3) + assert_eq!(metric, SolutionSize::Valid(0)); +} + +#[test] +fn test_minimum_tardiness_sequencing_serialization() { + let problem = MinimumTardinessSequencing::new(3, vec![2, 3, 1], vec![(0, 1)]); + let json = serde_json::to_value(&problem).unwrap(); + let restored: MinimumTardinessSequencing = serde_json::from_value(json).unwrap(); + assert_eq!(restored.num_tasks(), problem.num_tasks()); + assert_eq!(restored.deadlines(), problem.deadlines()); + assert_eq!(restored.precedences(), problem.precedences()); +} + +#[test] +fn test_minimum_tardiness_sequencing_empty() { + let problem = MinimumTardinessSequencing::new(0, vec![], vec![]); + assert_eq!(problem.num_tasks(), 0); + assert_eq!(problem.dims(), Vec::::new()); + assert_eq!(problem.evaluate(&[]), SolutionSize::Valid(0)); +} + +#[test] +fn test_minimum_tardiness_sequencing_single_task() { + let problem = MinimumTardinessSequencing::new(1, vec![1], vec![]); + assert_eq!(problem.dims(), vec![1]); + // Task at position 0, finishes at 1 <= 1, not tardy + assert_eq!(problem.evaluate(&[0]), SolutionSize::Valid(0)); + + let problem_tardy = MinimumTardinessSequencing::new(1, vec![0], vec![]); + // Task at position 0, finishes at 1 > 0, tardy + assert_eq!(problem_tardy.evaluate(&[0]), SolutionSize::Valid(1)); +} + +#[test] +#[should_panic(expected = "deadlines length must equal num_tasks")] +fn test_minimum_tardiness_sequencing_mismatched_deadlines() { + MinimumTardinessSequencing::new(3, vec![1, 2], vec![]); +} + +#[test] +#[should_panic(expected = "predecessor index 5 out of range")] +fn test_minimum_tardiness_sequencing_invalid_precedence() { + MinimumTardinessSequencing::new(3, vec![1, 2, 3], vec![(5, 0)]); +} + +#[test] +fn test_minimum_tardiness_sequencing_cyclic_precedences() { + // Cyclic precedences: 0 -> 1 -> 2 -> 0. No valid schedule exists. + let problem = MinimumTardinessSequencing::new( + 3, + vec![3, 3, 3], + vec![(0, 1), (1, 2), (2, 0)], + ); + let solver = BruteForce::new(); + assert!(solver.find_best(&problem).is_none()); +} diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index 5a5923f81..eee5dc642 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -126,6 +126,10 @@ fn test_all_problems_implement_trait_correctly() { &FlowShopScheduling::new(2, vec![vec![1, 2], vec![3, 4]], 10), "FlowShopScheduling", ); + check_problem_trait( + &MinimumTardinessSequencing::new(3, vec![2, 3, 1], vec![(0, 2)]), + "MinimumTardinessSequencing", + ); } #[test] @@ -163,6 +167,10 @@ fn test_direction() { Direction::Minimize ); assert_eq!(Factoring::new(6, 2, 2).direction(), Direction::Minimize); + assert_eq!( + MinimumTardinessSequencing::new(3, vec![2, 3, 1], vec![(0, 2)]).direction(), + Direction::Minimize + ); assert_eq!( BicliqueCover::new(BipartiteGraph::new(2, 2, vec![(0, 0)]), 1).direction(), Direction::Minimize