Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion problemreductions-cli/src/dispatch.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -247,6 +247,7 @@ pub fn load_problem(
},
"Knapsack" => deser_opt::<Knapsack>(data),
"MinimumFeedbackVertexSet" => deser_opt::<MinimumFeedbackVertexSet<i32>>(data),
"PrecedenceConstrainedScheduling" => deser_sat::<PrecedenceConstrainedScheduling>(data),
"SubsetSum" => deser_sat::<SubsetSum>(data),
_ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)),
}
Expand Down Expand Up @@ -310,6 +311,7 @@ pub fn serialize_any_problem(
},
"Knapsack" => try_ser::<Knapsack>(any),
"MinimumFeedbackVertexSet" => try_ser::<MinimumFeedbackVertexSet<i32>>(any),
"PrecedenceConstrainedScheduling" => try_ser::<PrecedenceConstrainedScheduling>(any),
"SubsetSum" => try_ser::<SubsetSum>(any),
_ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)),
}
Expand Down
1 change: 1 addition & 0 deletions problemreductions-cli/src/problem_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
3 changes: 3 additions & 0 deletions src/models/misc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
159 changes: 159 additions & 0 deletions src/models/misc/precedence_constrained_scheduling.rs
Original file line number Diff line number Diff line change
@@ -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<usize> {
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;
2 changes: 1 addition & 1 deletion src/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
135 changes: 135 additions & 0 deletions src/unit_tests/models/misc/precedence_constrained_scheduling.rs
Original file line number Diff line number Diff line change
@@ -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!(
<PrecedenceConstrainedScheduling as Problem>::NAME,
"PrecedenceConstrainedScheduling"
);
assert_eq!(
<PrecedenceConstrainedScheduling as Problem>::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::<usize>::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));
}
Loading