diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 49dd523c..1a370a75 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -1,6 +1,6 @@ use anyhow::{bail, Context, Result}; use problemreductions::models::algebraic::{ClosestVectorProblem, ILP}; -use problemreductions::models::misc::{BinPacking, Knapsack, SubsetSum}; +use problemreductions::models::misc::{BinPacking, Knapsack, PrecedenceConstrainedScheduling, SubsetSum}; use problemreductions::prelude::*; use problemreductions::rules::{MinimizeSteps, ReductionGraph}; use problemreductions::solvers::{BruteForce, ILPSolver, Solver}; @@ -247,6 +247,7 @@ pub fn load_problem( }, "Knapsack" => deser_opt::(data), "MinimumFeedbackVertexSet" => deser_opt::>(data), + "PrecedenceConstrainedScheduling" => deser_sat::(data), "SubsetSum" => deser_sat::(data), _ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)), } @@ -310,6 +311,7 @@ pub fn serialize_any_problem( }, "Knapsack" => try_ser::(any), "MinimumFeedbackVertexSet" => try_ser::>(any), + "PrecedenceConstrainedScheduling" => 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 2b6c8c73..8c896d85 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -55,6 +55,7 @@ pub fn resolve_alias(input: &str) -> String { "cvp" | "closestvectorproblem" => "ClosestVectorProblem".to_string(), "knapsack" => "Knapsack".to_string(), "fvs" | "minimumfeedbackvertexset" => "MinimumFeedbackVertexSet".to_string(), + "precedenceconstrainedscheduling" => "PrecedenceConstrainedScheduling".to_string(), "subsetsum" => "SubsetSum".to_string(), _ => input.to_string(), // pass-through for exact names } diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index 36ebe905..89761833 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -5,16 +5,19 @@ //! - [`Factoring`]: Integer factorization //! - [`Knapsack`]: 0-1 Knapsack (maximize value subject to weight capacity) //! - [`PaintShop`]: Minimize color switches in paint shop scheduling +//! - [`PrecedenceConstrainedScheduling`]: Schedule unit tasks on processors by deadline //! - [`SubsetSum`]: Find a subset summing to exactly a target value mod bin_packing; pub(crate) mod factoring; mod knapsack; pub(crate) mod paintshop; +mod precedence_constrained_scheduling; mod subset_sum; pub use bin_packing::BinPacking; pub use factoring::Factoring; pub use knapsack::Knapsack; pub use paintshop::PaintShop; +pub use precedence_constrained_scheduling::PrecedenceConstrainedScheduling; pub use subset_sum::SubsetSum; diff --git a/src/models/misc/precedence_constrained_scheduling.rs b/src/models/misc/precedence_constrained_scheduling.rs new file mode 100644 index 00000000..b35bfe08 --- /dev/null +++ b/src/models/misc/precedence_constrained_scheduling.rs @@ -0,0 +1,159 @@ +//! Precedence Constrained Scheduling problem implementation. +//! +//! Given unit-length tasks with precedence constraints, m processors, and a +//! deadline D, determine whether all tasks can be scheduled to meet D while +//! respecting precedences. NP-complete via reduction from 3SAT (Ullman, 1975). + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "PrecedenceConstrainedScheduling", + module_path: module_path!(), + description: "Schedule unit-length tasks on m processors by deadline D respecting precedence constraints", + fields: &[ + FieldInfo { name: "num_tasks", type_name: "usize", description: "Number of tasks n = |T|" }, + FieldInfo { name: "num_processors", type_name: "usize", description: "Number of processors m" }, + FieldInfo { name: "deadline", type_name: "usize", description: "Global deadline D" }, + FieldInfo { name: "precedences", type_name: "Vec<(usize, usize)>", description: "Precedence pairs (i, j) meaning task i must finish before task j starts" }, + ], + } +} + +/// The Precedence Constrained Scheduling problem. +/// +/// Given `n` unit-length tasks with precedence constraints (a partial order), +/// `m` processors, and a deadline `D`, determine whether there exists a schedule +/// assigning each task to a time slot in `{0, ..., D-1}` such that: +/// - At most `m` tasks are assigned to any single time slot +/// - For each precedence `(i, j)`: task `j` starts after task `i` completes, +/// i.e., `slot(j) >= slot(i) + 1` +/// +/// # Representation +/// +/// Each task has a variable in `{0, ..., D-1}` representing its assigned time slot. +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::misc::PrecedenceConstrainedScheduling; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// // 4 tasks, 2 processors, deadline 3, with t0 < t2 and t1 < t3 +/// let problem = PrecedenceConstrainedScheduling::new(4, 2, 3, vec![(0, 2), (1, 3)]); +/// let solver = BruteForce::new(); +/// let solution = solver.find_satisfying(&problem); +/// assert!(solution.is_some()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PrecedenceConstrainedScheduling { + num_tasks: usize, + num_processors: usize, + deadline: usize, + precedences: Vec<(usize, usize)>, +} + +impl PrecedenceConstrainedScheduling { + /// Create a new Precedence Constrained Scheduling instance. + /// + /// # Panics + /// + /// Panics if `num_processors` or `deadline` is zero (when `num_tasks > 0`), + /// or if any precedence index is out of bounds (>= num_tasks). + pub fn new( + num_tasks: usize, + num_processors: usize, + deadline: usize, + precedences: Vec<(usize, usize)>, + ) -> Self { + if num_tasks > 0 { + assert!(num_processors > 0, "num_processors must be > 0 when there are tasks"); + assert!(deadline > 0, "deadline must be > 0 when there are tasks"); + } + for &(i, j) in &precedences { + assert!( + i < num_tasks && j < num_tasks, + "Precedence ({}, {}) out of bounds for {} tasks", + i, + j, + num_tasks + ); + } + Self { + num_tasks, + num_processors, + deadline, + precedences, + } + } + + /// Get the number of tasks. + pub fn num_tasks(&self) -> usize { + self.num_tasks + } + + /// Get the number of processors. + pub fn num_processors(&self) -> usize { + self.num_processors + } + + /// Get the deadline. + pub fn deadline(&self) -> usize { + self.deadline + } + + /// Get the precedence constraints. + pub fn precedences(&self) -> &[(usize, usize)] { + &self.precedences + } +} + +impl Problem for PrecedenceConstrainedScheduling { + const NAME: &'static str = "PrecedenceConstrainedScheduling"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + vec![self.deadline; self.num_tasks] + } + + fn evaluate(&self, config: &[usize]) -> bool { + if config.len() != self.num_tasks { + return false; + } + // Check all values are valid time slots + if config.iter().any(|&v| v >= self.deadline) { + return false; + } + // Check processor capacity: at most num_processors tasks per time slot + let mut slot_count = vec![0usize; self.deadline]; + for &slot in config { + slot_count[slot] += 1; + if slot_count[slot] > self.num_processors { + return false; + } + } + // Check precedence constraints: for (i, j), slot[j] >= slot[i] + 1 + for &(i, j) in &self.precedences { + if config[j] < config[i] + 1 { + return false; + } + } + true + } +} + +impl SatisfactionProblem for PrecedenceConstrainedScheduling {} + +crate::declare_variants! { + PrecedenceConstrainedScheduling => "deadline ^ num_tasks", +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/precedence_constrained_scheduling.rs"] +mod tests; diff --git a/src/models/mod.rs b/src/models/mod.rs index ceb584ce..91266b5b 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -16,5 +16,5 @@ pub use graph::{ MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumFeedbackVertexSet, MinimumVertexCover, SpinGlass, TravelingSalesman, }; -pub use misc::{BinPacking, Factoring, Knapsack, PaintShop, SubsetSum}; +pub use misc::{BinPacking, Factoring, Knapsack, PaintShop, PrecedenceConstrainedScheduling, SubsetSum}; pub use set::{MaximumSetPacking, MinimumSetCovering}; diff --git a/src/unit_tests/models/misc/precedence_constrained_scheduling.rs b/src/unit_tests/models/misc/precedence_constrained_scheduling.rs new file mode 100644 index 00000000..8a3ace31 --- /dev/null +++ b/src/unit_tests/models/misc/precedence_constrained_scheduling.rs @@ -0,0 +1,135 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::traits::Problem; + +#[test] +fn test_precedence_constrained_scheduling_basic() { + let problem = PrecedenceConstrainedScheduling::new(4, 2, 3, vec![(0, 2), (1, 3)]); + assert_eq!(problem.num_tasks(), 4); + assert_eq!(problem.num_processors(), 2); + assert_eq!(problem.deadline(), 3); + assert_eq!(problem.precedences(), &[(0, 2), (1, 3)]); + assert_eq!(problem.dims(), vec![3; 4]); + assert_eq!( + ::NAME, + "PrecedenceConstrainedScheduling" + ); + assert_eq!( + ::variant(), + vec![] + ); +} + +#[test] +fn test_precedence_constrained_scheduling_evaluate_valid() { + // Issue example: 8 tasks, 3 processors, deadline 4 + // Precedences: 0<2, 0<3, 1<3, 1<4, 2<5, 3<6, 4<6, 5<7, 6<7 + let problem = PrecedenceConstrainedScheduling::new( + 8, + 3, + 4, + vec![ + (0, 2), + (0, 3), + (1, 3), + (1, 4), + (2, 5), + (3, 6), + (4, 6), + (5, 7), + (6, 7), + ], + ); + // Valid schedule: slot 0: {t0, t1}, slot 1: {t2, t3, t4}, slot 2: {t5, t6}, slot 3: {t7} + let config = vec![0, 0, 1, 1, 1, 2, 2, 3]; + assert!(problem.evaluate(&config)); +} + +#[test] +fn test_precedence_constrained_scheduling_evaluate_invalid_precedence() { + // t0 < t1, but we assign both to slot 0 + let problem = PrecedenceConstrainedScheduling::new(2, 2, 3, vec![(0, 1)]); + assert!(!problem.evaluate(&[0, 0])); // slot[1] = 0 < slot[0] + 1 = 1 +} + +#[test] +fn test_precedence_constrained_scheduling_evaluate_invalid_capacity() { + // 3 tasks, 2 processors, all in slot 0 + let problem = PrecedenceConstrainedScheduling::new(3, 2, 2, vec![]); + assert!(!problem.evaluate(&[0, 0, 0])); // 3 tasks in slot 0, capacity 2 +} + +#[test] +fn test_precedence_constrained_scheduling_evaluate_wrong_config_length() { + let problem = PrecedenceConstrainedScheduling::new(3, 2, 3, vec![]); + assert!(!problem.evaluate(&[0, 1])); + assert!(!problem.evaluate(&[0, 1, 2, 0])); +} + +#[test] +fn test_precedence_constrained_scheduling_evaluate_invalid_variable_value() { + let problem = PrecedenceConstrainedScheduling::new(2, 2, 3, vec![]); + assert!(!problem.evaluate(&[0, 3])); // 3 >= deadline=3 +} + +#[test] +fn test_precedence_constrained_scheduling_brute_force() { + // Small instance: 3 tasks, 2 processors, deadline 2, t0 < t2 + let problem = PrecedenceConstrainedScheduling::new(3, 2, 2, vec![(0, 2)]); + let solver = BruteForce::new(); + let solution = solver + .find_satisfying(&problem) + .expect("should find a solution"); + assert!(problem.evaluate(&solution)); +} + +#[test] +fn test_precedence_constrained_scheduling_brute_force_all() { + let problem = PrecedenceConstrainedScheduling::new(3, 2, 2, vec![(0, 2)]); + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + assert!(!solutions.is_empty()); + for sol in &solutions { + assert!(problem.evaluate(sol)); + } +} + +#[test] +fn test_precedence_constrained_scheduling_unsatisfiable() { + // 3 tasks in a chain t0 < t1 < t2, but only deadline 2 (need 3 slots) + let problem = PrecedenceConstrainedScheduling::new(3, 1, 2, vec![(0, 1), (1, 2)]); + let solver = BruteForce::new(); + assert!(solver.find_satisfying(&problem).is_none()); +} + +#[test] +fn test_precedence_constrained_scheduling_serialization() { + let problem = PrecedenceConstrainedScheduling::new(4, 2, 3, vec![(0, 2), (1, 3)]); + let json = serde_json::to_value(&problem).unwrap(); + let restored: PrecedenceConstrainedScheduling = serde_json::from_value(json).unwrap(); + assert_eq!(restored.num_tasks(), problem.num_tasks()); + assert_eq!(restored.num_processors(), problem.num_processors()); + assert_eq!(restored.deadline(), problem.deadline()); + assert_eq!(restored.precedences(), problem.precedences()); +} + +#[test] +fn test_precedence_constrained_scheduling_empty() { + let problem = PrecedenceConstrainedScheduling::new(0, 1, 1, vec![]); + assert_eq!(problem.num_tasks(), 0); + assert_eq!(problem.dims(), Vec::::new()); + assert!(problem.evaluate(&[])); +} + +#[test] +fn test_precedence_constrained_scheduling_no_precedences() { + // 4 tasks, 2 processors, deadline 2, no precedences + let problem = PrecedenceConstrainedScheduling::new(4, 2, 2, vec![]); + // 2 tasks per slot, 2 slots = 4 tasks + assert!(problem.evaluate(&[0, 0, 1, 1])); + let solver = BruteForce::new(); + let solution = solver + .find_satisfying(&problem) + .expect("should find a solution"); + assert!(problem.evaluate(&solution)); +}