From 4c3e73a7f4595b9b5b755409df45e012d7fec066 Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 11:36:11 +0000 Subject: [PATCH 1/6] Add plan for #220: MinimumTardinessSequencing Co-Authored-By: Claude Opus 4.6 --- ...2026-03-13-minimum-tardiness-sequencing.md | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 docs/plans/2026-03-13-minimum-tardiness-sequencing.md diff --git a/docs/plans/2026-03-13-minimum-tardiness-sequencing.md b/docs/plans/2026-03-13-minimum-tardiness-sequencing.md new file mode 100644 index 000000000..e0a5ef7f9 --- /dev/null +++ b/docs/plans/2026-03-13-minimum-tardiness-sequencing.md @@ -0,0 +1,79 @@ +# Plan: Add MinimumTardinessSequencing Model + +**Issue:** #220 +**Type:** [Model] +**Skill:** add-model + +## Summary + +Add `MinimumTardinessSequencing` -- a classical NP-complete single-machine scheduling problem (SS2 from Garey & Johnson) 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`. + +## Information Checklist + +| # | Item | Value | +|---|------|-------| +| 1 | Problem name | `MinimumTardinessSequencing` | +| 2 | Mathematical definition | Given tasks T with unit length, deadlines d(t), and partial order on T, find bijective schedule sigma minimizing tardy count | +| 3 | Problem type | Optimization (Minimize) | +| 4 | Type parameters | None | +| 5 | Struct fields | `num_tasks: usize`, `deadlines: Vec`, `precedences: Vec<(usize, usize)>` | +| 6 | Configuration space | `vec![num_tasks; num_tasks]` -- each task assigned a position 0..num_tasks | +| 7 | Feasibility check | Config must be a valid permutation AND respect precedence constraints | +| 8 | Objective function | Count of tardy tasks: |{t : sigma(t)+1 > d(t)}| | +| 9 | Best known exact algorithm | O(2^n * n) subset DP (Lawler et al. 1993) | +| 10 | Solving strategy | BruteForce (enumerate all configs, check validity) | +| 11 | Category | `misc` | + +## Implementation Steps + +### Batch 1 (parallel -- independent tasks) + +#### Step 1: Create model file `src/models/misc/minimum_tardiness_sequencing.rs` + +- Struct: `MinimumTardinessSequencing` with fields `num_tasks`, `deadlines`, `precedences` +- Constructor: `new(num_tasks, deadlines, precedences)` with validation +- Getters: `num_tasks()`, `num_precedences()`, `deadlines()`, `precedences()` +- `inventory::submit!` for `ProblemSchemaEntry` +- `Problem` impl: NAME = "MinimumTardinessSequencing", Metric = SolutionSize, dims = vec![num_tasks; num_tasks] +- `evaluate()`: check config length, check valid permutation (bijective), check precedence constraints, count tardy tasks +- `OptimizationProblem` impl: Value = usize, direction = Minimize +- `variant_params![]` (no type params) +- `declare_variants!`: `MinimumTardinessSequencing => "2^num_tasks"` +- `#[cfg(test)] #[path = "..."] mod tests;` + +#### Step 2: Create unit test file `src/unit_tests/models/misc/minimum_tardiness_sequencing.rs` + +- `test_minimum_tardiness_sequencing_basic`: construct instance, verify dims, direction, NAME, variant +- `test_minimum_tardiness_sequencing_evaluate_optimal`: verify the example from the issue (5 tasks, 1 tardy) +- `test_minimum_tardiness_sequencing_evaluate_invalid_permutation`: non-bijective config returns Invalid +- `test_minimum_tardiness_sequencing_evaluate_precedence_violation`: violating precedence returns Invalid +- `test_minimum_tardiness_sequencing_evaluate_all_on_time`: schedule where no tasks are tardy +- `test_minimum_tardiness_sequencing_brute_force`: verify BruteForce finds optimal +- `test_minimum_tardiness_sequencing_serialization`: round-trip serde +- `test_minimum_tardiness_sequencing_empty`: empty instance +- `test_minimum_tardiness_sequencing_no_precedences`: instance without precedences + +### Batch 2 (parallel -- registration tasks, depend on Batch 1) + +#### Step 3: Register model in module system + +- `src/models/misc/mod.rs`: add `mod minimum_tardiness_sequencing;` and `pub use` +- `src/models/mod.rs`: add to `misc` re-export line + +#### Step 4: Register in CLI dispatch + +- `problemreductions-cli/src/dispatch.rs`: add `use` import, add match arms in `load_problem()` and `serialize_any_problem()` +- `problemreductions-cli/src/problem_name.rs`: add `"minimumtardinesssequencing"` alias in `resolve_alias()` + +#### Step 5: Add CLI creation support + +- `problemreductions-cli/src/cli.rs`: add `--deadlines` and `--precedence-pairs` flags to `CreateArgs`, update `all_data_flags_empty()`, add to "Flags by problem type" help table +- `problemreductions-cli/src/commands/create.rs`: add match arm for `"MinimumTardinessSequencing"` + +### Batch 3 (sequential -- verification) + +#### Step 6: Verify + +- `make fmt` +- `make clippy` +- `make test` From 36047746d63e25ec697855d7be9a42b5bcb28aaa Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 11:42:50 +0000 Subject: [PATCH 2/6] Implement #220: Add MinimumTardinessSequencing model Add MinimumTardinessSequencing, a classical NP-complete single-machine scheduling problem (SS2 from Garey & Johnson) corresponding to the scheduling notation 1|prec, pj=1|sum Uj. - Model: num_tasks, deadlines, precedences with permutation-based configs - OptimizationProblem: minimize tardy task count (Value = usize) - Complexity: 2^num_tasks (subset DP baseline) - CLI: dispatch, alias, create support with --deadlines/--precedence-pairs - Tests: 15 unit tests covering evaluation, brute force, serialization Co-Authored-By: Claude Opus 4.6 --- problemreductions-cli/src/cli.rs | 7 + problemreductions-cli/src/commands/create.rs | 49 ++++- problemreductions-cli/src/dispatch.rs | 6 +- problemreductions-cli/src/problem_name.rs | 1 + src/lib.rs | 3 +- .../misc/minimum_tardiness_sequencing.rs | 186 ++++++++++++++++++ src/models/misc/mod.rs | 3 + src/models/mod.rs | 5 +- .../misc/minimum_tardiness_sequencing.rs | 159 +++++++++++++++ 9 files changed, 415 insertions(+), 4 deletions(-) create mode 100644 src/models/misc/minimum_tardiness_sequencing.rs create mode 100644 src/unit_tests/models/misc/minimum_tardiness_sequencing.rs diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 6b4cbb5be..dbd78c117 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -220,6 +220,7 @@ Flags by problem type: CVP --basis, --target-vec [--bounds] LCS --strings FVS --arcs [--weights] [--num-vertices] + MinimumTardinessSequencing --n, --deadlines [--precedence-pairs] ILP, CircuitSAT (via reduction only) Geometry graph variants (use slash notation, e.g., MIS/KingsSubgraph): @@ -337,6 +338,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, } #[derive(clap::Args)] diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 2b4cb04b3..e5843d12e 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -6,7 +6,9 @@ use crate::util; use anyhow::{bail, Context, Result}; use problemreductions::models::algebraic::{ClosestVectorProblem, BMF}; use problemreductions::models::graph::GraphPartitioning; -use problemreductions::models::misc::{BinPacking, LongestCommonSubsequence, PaintShop, SubsetSum}; +use problemreductions::models::misc::{ + BinPacking, LongestCommonSubsequence, MinimumTardinessSequencing, PaintShop, SubsetSum, +}; use problemreductions::prelude::*; use problemreductions::registry::collect_schemas; use problemreductions::topology::{ @@ -49,6 +51,8 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.bounds.is_none() && args.strings.is_none() && args.arcs.is_none() + && args.deadlines.is_none() + && args.precedence_pairs.is_none() } fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { @@ -502,6 +506,49 @@ 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![], + }; + ( + ser(MinimumTardinessSequencing::new( + num_tasks, + deadlines, + precedences, + ))?, + resolved_variant.clone(), + ) + } + // MinimumFeedbackVertexSet "MinimumFeedbackVertexSet" => { let arcs_str = args.arcs.as_ref().ok_or_else(|| { diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index e162efc24..fd316b244 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -1,6 +1,8 @@ use anyhow::{bail, Context, Result}; use problemreductions::models::algebraic::{ClosestVectorProblem, ILP}; -use problemreductions::models::misc::{BinPacking, Knapsack, LongestCommonSubsequence, SubsetSum}; +use problemreductions::models::misc::{ + BinPacking, Knapsack, LongestCommonSubsequence, MinimumTardinessSequencing, SubsetSum, +}; use problemreductions::prelude::*; use problemreductions::rules::{MinimizeSteps, ReductionGraph}; use problemreductions::solvers::{BruteForce, ILPSolver, Solver}; @@ -248,6 +250,7 @@ pub fn load_problem( "Knapsack" => deser_opt::(data), "LongestCommonSubsequence" => deser_opt::(data), "MinimumFeedbackVertexSet" => deser_opt::>(data), + "MinimumTardinessSequencing" => deser_opt::(data), "SubsetSum" => deser_sat::(data), _ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)), } @@ -312,6 +315,7 @@ pub fn serialize_any_problem( "Knapsack" => try_ser::(any), "LongestCommonSubsequence" => try_ser::(any), "MinimumFeedbackVertexSet" => try_ser::>(any), + "MinimumTardinessSequencing" => try_ser::(any), "SubsetSum" => try_ser::(any), _ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)), } diff --git a/problemreductions-cli/src/problem_name.rs b/problemreductions-cli/src/problem_name.rs index a595f61b6..2a3fe4927 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -57,6 +57,7 @@ pub fn resolve_alias(input: &str) -> String { "knapsack" => "Knapsack".to_string(), "lcs" | "longestcommonsubsequence" => "LongestCommonSubsequence".to_string(), "fvs" | "minimumfeedbackvertexset" => "MinimumFeedbackVertexSet".to_string(), + "minimumtardinesssequencing" => "MinimumTardinessSequencing".to_string(), "subsetsum" => "SubsetSum".to_string(), _ => input.to_string(), // pass-through for exact names } diff --git a/src/lib.rs b/src/lib.rs index bdcbf5f32..b8f17f2a2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,7 +46,8 @@ pub mod prelude { MinimumDominatingSet, MinimumFeedbackVertexSet, MinimumVertexCover, TravelingSalesman, }; pub use crate::models::misc::{ - BinPacking, Factoring, Knapsack, LongestCommonSubsequence, PaintShop, SubsetSum, + BinPacking, Factoring, Knapsack, LongestCommonSubsequence, MinimumTardinessSequencing, + PaintShop, SubsetSum, }; pub use crate::models::set::{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..acf79edb3 --- /dev/null +++ b/src/models/misc/minimum_tardiness_sequencing.rs @@ -0,0 +1,186 @@ +//! 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", + 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 { + vec![self.num_tasks; self.num_tasks] + } + + fn evaluate(&self, config: &[usize]) -> SolutionSize { + if config.len() != self.num_tasks { + return SolutionSize::Invalid; + } + + // Check that all values are in range + if config.iter().any(|&v| v >= self.num_tasks) { + return SolutionSize::Invalid; + } + + // Check bijection (valid permutation) + let mut seen = vec![false; self.num_tasks]; + for &pos in config { + if seen[pos] { + return SolutionSize::Invalid; + } + seen[pos] = true; + } + + // Check precedence constraints: for each (pred, succ), sigma(pred) < sigma(succ) + for &(pred, succ) in &self.precedences { + if config[pred] >= config[succ] { + return SolutionSize::Invalid; + } + } + + // Count tardy tasks: task t is tardy if sigma(t) + 1 > d(t) + let tardy_count = config + .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! { + MinimumTardinessSequencing => "2^num_tasks", +} + +#[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 943b758a2..24c74b85a 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -5,6 +5,7 @@ //! - [`Factoring`]: Integer factorization //! - [`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 //! - [`SubsetSum`]: Find a subset summing to exactly a target value @@ -12,6 +13,7 @@ mod bin_packing; pub(crate) mod factoring; mod knapsack; mod longest_common_subsequence; +mod minimum_tardiness_sequencing; pub(crate) mod paintshop; mod subset_sum; @@ -19,5 +21,6 @@ pub use bin_packing::BinPacking; pub use factoring::Factoring; pub use knapsack::Knapsack; pub use longest_common_subsequence::LongestCommonSubsequence; +pub use minimum_tardiness_sequencing::MinimumTardinessSequencing; pub use paintshop::PaintShop; pub use subset_sum::SubsetSum; diff --git a/src/models/mod.rs b/src/models/mod.rs index 6c8ac38a7..49fc0e78c 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -16,5 +16,8 @@ pub use graph::{ MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumFeedbackVertexSet, MinimumVertexCover, SpinGlass, TravelingSalesman, }; -pub use misc::{BinPacking, Factoring, Knapsack, LongestCommonSubsequence, PaintShop, SubsetSum}; +pub use misc::{ + BinPacking, Factoring, Knapsack, LongestCommonSubsequence, MinimumTardinessSequencing, + PaintShop, SubsetSum, +}; pub use set::{MaximumSetPacking, MinimumSetCovering}; diff --git a/src/unit_tests/models/misc/minimum_tardiness_sequencing.rs b/src/unit_tests/models/misc/minimum_tardiness_sequencing.rs new file mode 100644 index 000000000..2b96070d8 --- /dev/null +++ b/src/unit_tests/models/misc/minimum_tardiness_sequencing.rs @@ -0,0 +1,159 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::traits::{OptimizationProblem, Problem}; +use crate::types::Direction; + +#[test] +fn test_minimum_tardiness_sequencing_basic() { + let problem = MinimumTardinessSequencing::new( + 5, + vec![5, 5, 5, 3, 3], + vec![(0, 3), (1, 3), (1, 4), (2, 4)], + ); + assert_eq!(problem.num_tasks(), 5); + assert_eq!(problem.deadlines(), &[5, 5, 5, 3, 3]); + assert_eq!(problem.precedences(), &[(0, 3), (1, 3), (1, 4), (2, 4)]); + assert_eq!(problem.num_precedences(), 4); + assert_eq!(problem.dims(), vec![5; 5]); + assert_eq!(problem.direction(), Direction::Minimize); + assert_eq!( + ::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)], + ); + // Schedule: t0=0, t1=1, t3=2, t2=3, t4=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, 1, 3, 2, 4]; + assert_eq!(problem.evaluate(&config), SolutionSize::Valid(1)); +} + +#[test] +fn test_minimum_tardiness_sequencing_evaluate_invalid_permutation() { + let problem = MinimumTardinessSequencing::new(3, vec![2, 3, 1], vec![]); + // Not a permutation: position 0 used twice + assert_eq!(problem.evaluate(&[0, 0, 1]), SolutionSize::Invalid); +} + +#[test] +fn test_minimum_tardiness_sequencing_evaluate_out_of_range() { + let problem = MinimumTardinessSequencing::new(3, vec![2, 3, 1], vec![]); + 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 + ); + // Valid: t0 at pos 0, t1 at pos 1 -> ok + assert_eq!(problem.evaluate(&[0, 1, 2]), SolutionSize::Valid(0)); + // Invalid: t0 at pos 1, t1 at pos 0 -> violates precedence + assert_eq!(problem.evaluate(&[1, 0, 2]), SolutionSize::Invalid); + // Invalid: t0 at pos 2, t1 at pos 2 -> not a permutation (and would violate precedence) + assert_eq!(problem.evaluate(&[2, 2, 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 + assert_eq!(problem.evaluate(&[0, 1, 2]), SolutionSize::Valid(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![]); + // pos 0 finishes at 1 > 0 (tardy), pos 1 finishes at 2 > 0 (tardy) + assert_eq!(problem.evaluate(&[0, 1]), 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, 2, 1. 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)]); +} From e312e5131f110948778f2f4fcfcb1d503a105d65 Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 11:42:54 +0000 Subject: [PATCH 3/6] chore: remove plan file after implementation Co-Authored-By: Claude Opus 4.6 --- ...2026-03-13-minimum-tardiness-sequencing.md | 79 ------------------- 1 file changed, 79 deletions(-) delete mode 100644 docs/plans/2026-03-13-minimum-tardiness-sequencing.md diff --git a/docs/plans/2026-03-13-minimum-tardiness-sequencing.md b/docs/plans/2026-03-13-minimum-tardiness-sequencing.md deleted file mode 100644 index e0a5ef7f9..000000000 --- a/docs/plans/2026-03-13-minimum-tardiness-sequencing.md +++ /dev/null @@ -1,79 +0,0 @@ -# Plan: Add MinimumTardinessSequencing Model - -**Issue:** #220 -**Type:** [Model] -**Skill:** add-model - -## Summary - -Add `MinimumTardinessSequencing` -- a classical NP-complete single-machine scheduling problem (SS2 from Garey & Johnson) 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`. - -## Information Checklist - -| # | Item | Value | -|---|------|-------| -| 1 | Problem name | `MinimumTardinessSequencing` | -| 2 | Mathematical definition | Given tasks T with unit length, deadlines d(t), and partial order on T, find bijective schedule sigma minimizing tardy count | -| 3 | Problem type | Optimization (Minimize) | -| 4 | Type parameters | None | -| 5 | Struct fields | `num_tasks: usize`, `deadlines: Vec`, `precedences: Vec<(usize, usize)>` | -| 6 | Configuration space | `vec![num_tasks; num_tasks]` -- each task assigned a position 0..num_tasks | -| 7 | Feasibility check | Config must be a valid permutation AND respect precedence constraints | -| 8 | Objective function | Count of tardy tasks: |{t : sigma(t)+1 > d(t)}| | -| 9 | Best known exact algorithm | O(2^n * n) subset DP (Lawler et al. 1993) | -| 10 | Solving strategy | BruteForce (enumerate all configs, check validity) | -| 11 | Category | `misc` | - -## Implementation Steps - -### Batch 1 (parallel -- independent tasks) - -#### Step 1: Create model file `src/models/misc/minimum_tardiness_sequencing.rs` - -- Struct: `MinimumTardinessSequencing` with fields `num_tasks`, `deadlines`, `precedences` -- Constructor: `new(num_tasks, deadlines, precedences)` with validation -- Getters: `num_tasks()`, `num_precedences()`, `deadlines()`, `precedences()` -- `inventory::submit!` for `ProblemSchemaEntry` -- `Problem` impl: NAME = "MinimumTardinessSequencing", Metric = SolutionSize, dims = vec![num_tasks; num_tasks] -- `evaluate()`: check config length, check valid permutation (bijective), check precedence constraints, count tardy tasks -- `OptimizationProblem` impl: Value = usize, direction = Minimize -- `variant_params![]` (no type params) -- `declare_variants!`: `MinimumTardinessSequencing => "2^num_tasks"` -- `#[cfg(test)] #[path = "..."] mod tests;` - -#### Step 2: Create unit test file `src/unit_tests/models/misc/minimum_tardiness_sequencing.rs` - -- `test_minimum_tardiness_sequencing_basic`: construct instance, verify dims, direction, NAME, variant -- `test_minimum_tardiness_sequencing_evaluate_optimal`: verify the example from the issue (5 tasks, 1 tardy) -- `test_minimum_tardiness_sequencing_evaluate_invalid_permutation`: non-bijective config returns Invalid -- `test_minimum_tardiness_sequencing_evaluate_precedence_violation`: violating precedence returns Invalid -- `test_minimum_tardiness_sequencing_evaluate_all_on_time`: schedule where no tasks are tardy -- `test_minimum_tardiness_sequencing_brute_force`: verify BruteForce finds optimal -- `test_minimum_tardiness_sequencing_serialization`: round-trip serde -- `test_minimum_tardiness_sequencing_empty`: empty instance -- `test_minimum_tardiness_sequencing_no_precedences`: instance without precedences - -### Batch 2 (parallel -- registration tasks, depend on Batch 1) - -#### Step 3: Register model in module system - -- `src/models/misc/mod.rs`: add `mod minimum_tardiness_sequencing;` and `pub use` -- `src/models/mod.rs`: add to `misc` re-export line - -#### Step 4: Register in CLI dispatch - -- `problemreductions-cli/src/dispatch.rs`: add `use` import, add match arms in `load_problem()` and `serialize_any_problem()` -- `problemreductions-cli/src/problem_name.rs`: add `"minimumtardinesssequencing"` alias in `resolve_alias()` - -#### Step 5: Add CLI creation support - -- `problemreductions-cli/src/cli.rs`: add `--deadlines` and `--precedence-pairs` flags to `CreateArgs`, update `all_data_flags_empty()`, add to "Flags by problem type" help table -- `problemreductions-cli/src/commands/create.rs`: add match arm for `"MinimumTardinessSequencing"` - -### Batch 3 (sequential -- verification) - -#### Step 6: Verify - -- `make fmt` -- `make clippy` -- `make test` From 8b21538b3cdddd7353263de0920a0ab824d97009 Mon Sep 17 00:00:00 2001 From: zazabap Date: Sun, 15 Mar 2026 08:46:19 +0000 Subject: [PATCH 4/6] fix: address Copilot review comments - Fix declare_variants! syntax (add default opt) - Add missing ProblemSchemaEntry fields (aliases, dimensions, display_name) - Fix test comment to match actual data (deadlines 1,3,2 not 1,2,1) - Add CLI input validation for deadlines/precedences - Regenerate docs JSON (problem_schemas.json, reduction_graph.json) Co-Authored-By: Claude Opus 4.6 --- docs/src/reductions/problem_schemas.json | 21 ++++++ docs/src/reductions/reduction_graph.json | 65 ++++++++++--------- problemreductions-cli/src/commands/create.rs | 15 +++++ .../misc/minimum_tardiness_sequencing.rs | 5 +- .../misc/minimum_tardiness_sequencing.rs | 2 +- 5 files changed, 77 insertions(+), 31 deletions(-) diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index 43b2a4456..3292912a3 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -445,6 +445,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 bb8c02551..9b69edd13 100644 --- a/docs/src/reductions/reduction_graph.json +++ b/docs/src/reductions/reduction_graph.json @@ -389,6 +389,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": { @@ -535,7 +542,7 @@ }, { "source": 4, - "target": 52, + "target": 53, "overhead": [ { "field": "num_spins", @@ -595,7 +602,7 @@ }, { "source": 11, - "target": 47, + "target": 48, "overhead": [ { "field": "num_vars", @@ -636,7 +643,7 @@ }, { "source": 18, - "target": 47, + "target": 48, "overhead": [ { "field": "num_vars", @@ -662,7 +669,7 @@ }, { "source": 19, - "target": 47, + "target": 48, "overhead": [ { "field": "num_vars", @@ -688,7 +695,7 @@ }, { "source": 20, - "target": 47, + "target": 48, "overhead": [ { "field": "num_vars", @@ -699,7 +706,7 @@ }, { "source": 20, - "target": 54, + "target": 55, "overhead": [ { "field": "num_elements", @@ -710,7 +717,7 @@ }, { "source": 21, - "target": 49, + "target": 50, "overhead": [ { "field": "num_clauses", @@ -729,7 +736,7 @@ }, { "source": 22, - "target": 47, + "target": 48, "overhead": [ { "field": "num_vars", @@ -755,7 +762,7 @@ }, { "source": 24, - "target": 52, + "target": 53, "overhead": [ { "field": "num_spins", @@ -935,7 +942,7 @@ }, { "source": 30, - "target": 43, + "target": 44, "overhead": [ { "field": "num_vertices", @@ -1070,7 +1077,7 @@ }, { "source": 36, - "target": 47, + "target": 48, "overhead": [ { "field": "num_vars", @@ -1155,7 +1162,7 @@ "doc_path": "rules/minimumsetcovering_ilp/index.html" }, { - "source": 43, + "source": 44, "target": 30, "overhead": [ { @@ -1170,7 +1177,7 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 43, + "source": 44, "target": 41, "overhead": [ { @@ -1185,7 +1192,7 @@ "doc_path": "rules/minimumvertexcover_minimumsetcovering/index.html" }, { - "source": 47, + "source": 48, "target": 11, "overhead": [ { @@ -1200,8 +1207,8 @@ "doc_path": "rules/qubo_ilp/index.html" }, { - "source": 47, - "target": 51, + "source": 48, + "target": 52, "overhead": [ { "field": "num_spins", @@ -1211,7 +1218,7 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 49, + "source": 50, "target": 4, "overhead": [ { @@ -1226,7 +1233,7 @@ "doc_path": "rules/sat_circuitsat/index.html" }, { - "source": 49, + "source": 50, "target": 15, "overhead": [ { @@ -1241,7 +1248,7 @@ "doc_path": "rules/sat_coloring/index.html" }, { - "source": 49, + "source": 50, "target": 20, "overhead": [ { @@ -1256,7 +1263,7 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 49, + "source": 50, "target": 29, "overhead": [ { @@ -1271,7 +1278,7 @@ "doc_path": "rules/sat_maximumindependentset/index.html" }, { - "source": 49, + "source": 50, "target": 38, "overhead": [ { @@ -1286,8 +1293,8 @@ "doc_path": "rules/sat_minimumdominatingset/index.html" }, { - "source": 51, - "target": 47, + "source": 52, + "target": 48, "overhead": [ { "field": "num_vars", @@ -1297,7 +1304,7 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 52, + "source": 53, "target": 24, "overhead": [ { @@ -1312,8 +1319,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 52, - "target": 51, + "source": 53, + "target": 52, "overhead": [ { "field": "num_spins", @@ -1327,7 +1334,7 @@ "doc_path": "rules/spinglass_casts/index.html" }, { - "source": 55, + "source": 56, "target": 11, "overhead": [ { @@ -1342,8 +1349,8 @@ "doc_path": "rules/travelingsalesman_ilp/index.html" }, { - "source": 55, - "target": 47, + "source": 56, + "target": 48, "overhead": [ { "field": "num_vars", diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 086dc2803..470a3a56c 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -779,6 +779,21 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { .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, diff --git a/src/models/misc/minimum_tardiness_sequencing.rs b/src/models/misc/minimum_tardiness_sequencing.rs index acf79edb3..13d7c49f5 100644 --- a/src/models/misc/minimum_tardiness_sequencing.rs +++ b/src/models/misc/minimum_tardiness_sequencing.rs @@ -13,6 +13,9 @@ 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: &[ @@ -178,7 +181,7 @@ impl OptimizationProblem for MinimumTardinessSequencing { } crate::declare_variants! { - MinimumTardinessSequencing => "2^num_tasks", + default opt MinimumTardinessSequencing => "2^num_tasks", } #[cfg(test)] diff --git a/src/unit_tests/models/misc/minimum_tardiness_sequencing.rs b/src/unit_tests/models/misc/minimum_tardiness_sequencing.rs index 2b96070d8..fb2577bda 100644 --- a/src/unit_tests/models/misc/minimum_tardiness_sequencing.rs +++ b/src/unit_tests/models/misc/minimum_tardiness_sequencing.rs @@ -107,7 +107,7 @@ fn test_minimum_tardiness_sequencing_brute_force() { #[test] fn test_minimum_tardiness_sequencing_brute_force_no_precedences() { // Without precedences, Moore's algorithm gives optimal - // 3 tasks: deadlines 1, 2, 1. Best is to schedule task with deadline 1 first. + // 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"); From 3299bbc7c48241ca35c1a2ec3901732d0ac6d827 Mon Sep 17 00:00:00 2001 From: zazabap Date: Sun, 15 Mar 2026 08:54:04 +0000 Subject: [PATCH 5/6] fix: address structural review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Switch to Lehmer code encoding for permutations (dims=[n,n-1,...,1]) matching FlowShopScheduling pattern — reduces brute-force from n^n to n! - Add canonical model example in example_db - Add trait_consistency entry (Problem trait + Direction::Minimize) - Add paper problem-def and display-name entries - Add cyclic precedences test - Regenerate docs JSON Co-Authored-By: Claude Opus 4.6 --- docs/paper/reductions.typ | 11 ++++ .../misc/minimum_tardiness_sequencing.rs | 52 ++++++++++++++----- src/models/misc/mod.rs | 1 + .../misc/minimum_tardiness_sequencing.rs | 47 ++++++++++++----- src/unit_tests/trait_consistency.rs | 8 +++ 5 files changed, 91 insertions(+), 28 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 2b632fad6..778163a92 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -83,6 +83,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 @@ -1335,6 +1336,16 @@ Biclique Cover is equivalent to factoring the biadjacency matrix $M$ of the bipa ) ] +#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. + + The search space consists of all topological orderings of the partial order, and the objective counts the number of late tasks. For $n$ tasks with no precedence constraints, the brute-force complexity is $O(n!)$. +] + // Completeness check: warn about problem types in JSON but missing from paper #{ let json-models = { diff --git a/src/models/misc/minimum_tardiness_sequencing.rs b/src/models/misc/minimum_tardiness_sequencing.rs index 13d7c49f5..ed383c816 100644 --- a/src/models/misc/minimum_tardiness_sequencing.rs +++ b/src/models/misc/minimum_tardiness_sequencing.rs @@ -132,37 +132,43 @@ impl Problem for MinimumTardinessSequencing { } fn dims(&self) -> Vec { - vec![self.num_tasks; self.num_tasks] + let n = self.num_tasks; + (0..n).rev().map(|i| i + 1).collect() } fn evaluate(&self, config: &[usize]) -> SolutionSize { - if config.len() != self.num_tasks { + let n = self.num_tasks; + if config.len() != n { return SolutionSize::Invalid; } - // Check that all values are in range - if config.iter().any(|&v| v >= self.num_tasks) { - return SolutionSize::Invalid; - } - - // Check bijection (valid permutation) - let mut seen = vec![false; self.num_tasks]; - for &pos in config { - if seen[pos] { + // 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; } - seen[pos] = true; + 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 config[pred] >= config[succ] { + if sigma[pred] >= sigma[succ] { return SolutionSize::Invalid; } } // Count tardy tasks: task t is tardy if sigma(t) + 1 > d(t) - let tardy_count = config + let tardy_count = sigma .iter() .enumerate() .filter(|&(t, &pos)| pos + 1 > self.deadlines[t]) @@ -184,6 +190,24 @@ 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 91523bb47..cc96aa83e 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -37,5 +37,6 @@ pub(crate) fn canonical_model_example_specs() -> Vec::NAME, @@ -31,22 +31,26 @@ fn test_minimum_tardiness_sequencing_evaluate_optimal() { vec![5, 5, 5, 3, 3], vec![(0, 3), (1, 3), (1, 4), (2, 4)], ); - // Schedule: t0=0, t1=1, t3=2, t2=3, t4=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, 1, 3, 2, 4]; + let config = vec![0, 0, 1, 0, 0]; assert_eq!(problem.evaluate(&config), SolutionSize::Valid(1)); } #[test] -fn test_minimum_tardiness_sequencing_evaluate_invalid_permutation() { +fn test_minimum_tardiness_sequencing_evaluate_invalid_lehmer() { let problem = MinimumTardinessSequencing::new(3, vec![2, 3, 1], vec![]); - // Not a permutation: position 0 used twice - assert_eq!(problem.evaluate(&[0, 0, 1]), SolutionSize::Invalid); + // 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); } @@ -64,19 +68,21 @@ fn test_minimum_tardiness_sequencing_evaluate_precedence_violation() { vec![3, 3, 3], vec![(0, 1)], // task 0 must precede task 1 ); - // Valid: t0 at pos 0, t1 at pos 1 -> ok - assert_eq!(problem.evaluate(&[0, 1, 2]), SolutionSize::Valid(0)); - // Invalid: t0 at pos 1, t1 at pos 0 -> violates precedence - assert_eq!(problem.evaluate(&[1, 0, 2]), SolutionSize::Invalid); - // Invalid: t0 at pos 2, t1 at pos 2 -> not a permutation (and would violate precedence) - assert_eq!(problem.evaluate(&[2, 2, 0]), SolutionSize::Invalid); + // 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 - assert_eq!(problem.evaluate(&[0, 1, 2]), SolutionSize::Valid(0)); + // 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)); } @@ -86,8 +92,9 @@ fn test_minimum_tardiness_sequencing_evaluate_all_tardy() { // 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, 1]), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(&[0, 0]), SolutionSize::Valid(2)); } #[test] @@ -157,3 +164,15 @@ fn test_minimum_tardiness_sequencing_mismatched_deadlines() { 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 ebbc68a0e..f5408902e 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -122,6 +122,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] @@ -159,6 +163,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 From 9b33cb8e8bd1c0c515869b6a3afe20b58415a0e9 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 15 Mar 2026 19:52:48 +0800 Subject: [PATCH 6/6] fix: add cli.md example and Moore 1968 bibliography entry - Add concrete pred create MinimumTardinessSequencing example to docs/src/cli.md - Add Moore (1968) bibliography entry for the @moore1968 citation in the paper Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/paper/references.bib | 11 +++++++++++ docs/src/cli.md | 2 ++ 2 files changed, 13 insertions(+) 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 ca43e7926..6507c61f5 100644 --- a/docs/src/cli.md +++ b/docs/src/cli.md @@ -274,6 +274,8 @@ pred create SpinGlass --graph 0-1,1-2 -o sg.json 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.