diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 57fbf4fd6..98541db14 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -63,6 +63,7 @@ "MinimumSumMulticenter": [Minimum Sum Multicenter], "SubgraphIsomorphism": [Subgraph Isomorphism], "SubsetSum": [Subset Sum], + "FlowShopScheduling": [Flow Shop Scheduling], ) // Definition label: "def:" — each definition block must have a matching label @@ -1107,6 +1108,77 @@ Biclique Cover is equivalent to factoring the biadjacency matrix $M$ of the bipa *Example.* Consider $G$ with $V = {0, 1, 2, 3, 4, 5}$ and arcs $(0 arrow 1), (1 arrow 2), (2 arrow 0), (1 arrow 3), (3 arrow 4), (4 arrow 1), (2 arrow 5), (5 arrow 3), (3 arrow 0)$. This graph contains four directed cycles: $0 arrow 1 arrow 2 arrow 0$, $1 arrow 3 arrow 4 arrow 1$, $0 arrow 1 arrow 3 arrow 0$, and $2 arrow 5 arrow 3 arrow 0 arrow 1 arrow 2$. Removing $A' = {(0 arrow 1), (3 arrow 4)}$ breaks all four cycles (vertex 0 becomes a sink in the residual graph), giving a minimum FAS of size 2. ] +#problem-def("FlowShopScheduling")[ + Given $m$ processors and a set $J$ of $n$ jobs, where each job $j in J$ consists of $m$ tasks $t_1 [j], t_2 [j], dots, t_m [j]$ with lengths $ell(t_i [j]) in ZZ^+_0$, and a deadline $D in ZZ^+$, determine whether there exists a permutation schedule $pi$ of the jobs such that all jobs complete by time $D$. Each job must be processed on machines $1, 2, dots, m$ in order, and job $j$ cannot start on machine $i+1$ until its task on machine $i$ is completed. +][ + Flow Shop Scheduling is a classical NP-complete problem from Garey & Johnson (A5 SS15), strongly NP-hard for $m >= 3$ @garey1976. For $m = 2$, it is solvable in $O(n log n)$ by Johnson's rule @johnson1954. The problem is fundamental in operations research, manufacturing planning, and VLSI design. When restricted to permutation schedules (same job order on all machines), the search space is $n!$ orderings. The best known exact algorithm for $m = 3$ runs in $O^*(3^n)$ time @shang2018; for general $m$, brute-force over $n!$ permutations gives $O(n! dot m n)$. + + *Example.* Let $m = 3$ machines, $n = 5$ jobs with task lengths: + $ ell = mat( + 3, 4, 2; + 2, 3, 5; + 4, 1, 3; + 1, 5, 4; + 3, 2, 3; + ) $ + and deadline $D = 25$. The job order $pi = (j_4, j_1, j_5, j_3, j_2)$ (0-indexed: $3, 0, 4, 2, 1$) yields makespan $23 <= 25$, so a feasible schedule exists. + + #figure( + canvas(length: 1cm, { + import draw: * + // Gantt chart for job order [3, 0, 4, 2, 1] on 3 machines + // Schedule computed greedily: + // M1: j3[0,1], j0[1,4], j4[4,7], j2[7,11], j1[11,13] + // M2: j3[1,6], j0[6,10], j4[10,12], j2[12,13], j1[13,16] + // M3: j3[6,10], j0[10,12], j4[12,15], j2[15,18], j1[18,23] + let colors = (rgb("#4e79a7"), rgb("#e15759"), rgb("#76b7b2"), rgb("#f28e2b"), rgb("#59a14f")) + let job-names = ("$j_1$", "$j_2$", "$j_3$", "$j_4$", "$j_5$") + let scale = 0.38 + let row-h = 0.6 + let gap = 0.15 + + // Machine labels + for (mi, label) in ("M1", "M2", "M3").enumerate() { + let y = -mi * (row-h + gap) + content((-0.8, y), text(8pt, label)) + } + + // Draw schedule blocks: (machine, job-index, start, end) + let blocks = ( + (0, 3, 0, 1), (0, 0, 1, 4), (0, 4, 4, 7), (0, 2, 7, 11), (0, 1, 11, 13), + (1, 3, 1, 6), (1, 0, 6, 10), (1, 4, 10, 12), (1, 2, 12, 13), (1, 1, 13, 16), + (2, 3, 6, 10), (2, 0, 10, 12), (2, 4, 12, 15), (2, 2, 15, 18), (2, 1, 18, 23), + ) + + for (mi, ji, s, e) in blocks { + let x0 = s * scale + let x1 = e * scale + let y = -mi * (row-h + gap) + rect((x0, y - row-h / 2), (x1, y + row-h / 2), + fill: colors.at(ji).transparentize(30%), stroke: 0.4pt + colors.at(ji)) + content(((x0 + x1) / 2, y), text(6pt, job-names.at(ji))) + } + + // Time axis + let max-t = 23 + let y-axis = -2 * (row-h + gap) - row-h / 2 - 0.2 + line((0, y-axis), (max-t * scale, y-axis), stroke: 0.4pt) + for t in (0, 5, 10, 15, 20, 23) { + 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))) + } + content((max-t * scale / 2, y-axis - 0.5), text(7pt)[$t$]) + + // Deadline marker + let dl-x = 25 * scale + line((dl-x, row-h / 2 + 0.1), (dl-x, y-axis), stroke: (paint: red, thickness: 0.8pt, dash: "dashed")) + content((dl-x, row-h / 2 + 0.25), text(6pt, fill: red)[$D = 25$]) + }), + caption: [Flow shop schedule for 5 jobs on 3 machines. Job order $(j_4, j_1, j_5, j_3, j_2)$ achieves makespan 23, within deadline $D = 25$ (dashed red line).], + ) +] + // Completeness check: warn about problem types in JSON but missing from paper #{ let json-models = { diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 5ab08a430..7294009a2 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -224,6 +224,7 @@ Flags by problem type: LCS --strings FAS --arcs [--weights] [--num-vertices] FVS --arcs [--weights] [--num-vertices] + FlowShopScheduling --task-lengths, --deadline [--num-processors] SCS --strings, --bound [--alphabet-size] ILP, CircuitSAT (via reduction only) @@ -351,6 +352,15 @@ pub struct CreateArgs { /// Directed arcs for directed graph problems (e.g., 0>1,1>2,2>0) #[arg(long)] pub arcs: Option, + /// Task lengths for FlowShopScheduling (semicolon-separated rows: "3,4,2;2,3,5;4,1,3") + #[arg(long)] + pub task_lengths: Option, + /// Deadline for FlowShopScheduling + #[arg(long)] + pub deadline: Option, + /// Number of processors/machines for FlowShopScheduling + #[arg(long)] + pub num_processors: Option, /// Alphabet size for SCS (optional; inferred from max symbol + 1 if omitted) #[arg(long)] pub alphabet_size: Option, diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index a38ff2902..969ca9eab 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -7,7 +7,8 @@ use anyhow::{bail, Context, Result}; use problemreductions::models::algebraic::{ClosestVectorProblem, BMF}; use problemreductions::models::graph::{GraphPartitioning, HamiltonianPath}; use problemreductions::models::misc::{ - BinPacking, LongestCommonSubsequence, PaintShop, ShortestCommonSupersequence, SubsetSum, + BinPacking, FlowShopScheduling, LongestCommonSubsequence, PaintShop, + ShortestCommonSupersequence, SubsetSum, }; use problemreductions::prelude::*; use problemreductions::registry::collect_schemas; @@ -54,6 +55,9 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.pattern.is_none() && args.strings.is_none() && args.arcs.is_none() + && args.task_lengths.is_none() + && args.deadline.is_none() + && args.num_processors.is_none() && args.alphabet_size.is_none() } @@ -567,6 +571,49 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // FlowShopScheduling + "FlowShopScheduling" => { + let task_str = args.task_lengths.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "FlowShopScheduling requires --task-lengths and --deadline\n\n\ + Usage: pred create FlowShopScheduling --task-lengths \"3,4,2;2,3,5;4,1,3\" --deadline 25 --num-processors 3" + ) + })?; + let deadline = args.deadline.ok_or_else(|| { + anyhow::anyhow!( + "FlowShopScheduling requires --deadline\n\n\ + Usage: pred create FlowShopScheduling --task-lengths \"3,4,2;2,3,5;4,1,3\" --deadline 25 --num-processors 3" + ) + })?; + let task_lengths: Vec> = task_str + .split(';') + .map(|row| util::parse_comma_list(row.trim())) + .collect::>>()?; + let num_processors = if let Some(np) = args.num_processors { + np + } else if let Some(m) = args.m { + m + } else if let Some(first) = task_lengths.first() { + first.len() + } else { + bail!("Cannot infer num_processors from empty task list; use --num-processors"); + }; + for (j, row) in task_lengths.iter().enumerate() { + if row.len() != num_processors { + bail!( + "task_lengths row {} has {} entries, expected {} (num_processors)", + j, + row.len(), + num_processors + ); + } + } + ( + ser(FlowShopScheduling::new(num_processors, task_lengths, deadline))?, + resolved_variant.clone(), + ) + } + // MinimumFeedbackArcSet "MinimumFeedbackArcSet" => { let arcs_str = args.arcs.as_deref().ok_or_else(|| { diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 128d2734f..37ca0a957 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -1,7 +1,8 @@ use anyhow::{bail, Context, Result}; use problemreductions::models::algebraic::{ClosestVectorProblem, ILP}; use problemreductions::models::misc::{ - BinPacking, Knapsack, LongestCommonSubsequence, ShortestCommonSupersequence, SubsetSum, + BinPacking, FlowShopScheduling, Knapsack, LongestCommonSubsequence, + ShortestCommonSupersequence, SubsetSum, }; use problemreductions::prelude::*; use problemreductions::rules::{MinimizeSteps, ReductionGraph}; @@ -255,6 +256,7 @@ pub fn load_problem( "PartitionIntoTriangles" => deser_sat::>(data), "LongestCommonSubsequence" => deser_opt::(data), "MinimumFeedbackVertexSet" => deser_opt::>(data), + "FlowShopScheduling" => deser_sat::(data), "SubsetSum" => deser_sat::(data), "ShortestCommonSupersequence" => deser_sat::(data), "MinimumFeedbackArcSet" => deser_opt::>(data), @@ -326,6 +328,7 @@ pub fn serialize_any_problem( "PartitionIntoTriangles" => try_ser::>(any), "LongestCommonSubsequence" => try_ser::(any), "MinimumFeedbackVertexSet" => try_ser::>(any), + "FlowShopScheduling" => try_ser::(any), "SubsetSum" => try_ser::(any), "ShortestCommonSupersequence" => try_ser::(any), "MinimumFeedbackArcSet" => try_ser::>(any), diff --git a/problemreductions-cli/src/problem_name.rs b/problemreductions-cli/src/problem_name.rs index ffd9094a5..1249319e6 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -64,6 +64,7 @@ pub fn resolve_alias(input: &str) -> String { "partitionintotriangles" => "PartitionIntoTriangles".to_string(), "lcs" | "longestcommonsubsequence" => "LongestCommonSubsequence".to_string(), "fvs" | "minimumfeedbackvertexset" => "MinimumFeedbackVertexSet".to_string(), + "flowshopscheduling" => "FlowShopScheduling".to_string(), "fas" | "minimumfeedbackarcset" => "MinimumFeedbackArcSet".to_string(), "minimumsummulticenter" | "pmedian" => "MinimumSumMulticenter".to_string(), "subsetsum" => "SubsetSum".to_string(), diff --git a/src/lib.rs b/src/lib.rs index b66a5bdfe..94a54801f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,7 +50,7 @@ pub mod prelude { RuralPostman, TravelingSalesman, }; pub use crate::models::misc::{ - BinPacking, Factoring, Knapsack, LongestCommonSubsequence, PaintShop, + BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, PaintShop, ShortestCommonSupersequence, SubsetSum, }; pub use crate::models::set::{MaximumSetPacking, MinimumSetCovering}; diff --git a/src/models/misc/flow_shop_scheduling.rs b/src/models/misc/flow_shop_scheduling.rs new file mode 100644 index 000000000..6568cced8 --- /dev/null +++ b/src/models/misc/flow_shop_scheduling.rs @@ -0,0 +1,201 @@ +//! Flow Shop Scheduling problem implementation. +//! +//! Given m processors and a set of jobs, each consisting of m tasks (one per processor) +//! that must be processed in processor order 1, 2, ..., m, determine if all jobs can +//! be completed by a global deadline D. + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "FlowShopScheduling", + module_path: module_path!(), + description: "Determine if a flow-shop schedule for jobs on m processors meets a deadline", + fields: &[ + FieldInfo { name: "num_processors", type_name: "usize", description: "Number of machines m" }, + FieldInfo { name: "task_lengths", type_name: "Vec>", description: "task_lengths[j][i] = length of job j's task on machine i" }, + FieldInfo { name: "deadline", type_name: "u64", description: "Global deadline D" }, + ], + } +} + +/// The Flow Shop Scheduling problem. +/// +/// Given `m` processors and a set of `n` jobs, each job `j` consists of `m` tasks +/// `t_1[j], t_2[j], ..., t_m[j]` with specified lengths. Tasks must be processed +/// in processor order: job `j` cannot start on machine `i+1` until its task on +/// machine `i` is completed. The question is whether there exists a schedule such +/// that all jobs complete by deadline `D`. +/// +/// # Representation +/// +/// Configurations use Lehmer code encoding with `dims() = [n, n-1, ..., 1]`. +/// A config `[c_0, c_1, ..., c_{n-1}]` where `c_i < n - i` is decoded by +/// maintaining a list of available jobs and picking the `c_i`-th element: +/// +/// For 3 jobs, config `[2, 0, 0]`: available=`[0,1,2]`, pick index 2 → job 2; +/// available=`[0,1]`, pick index 0 → job 0; available=`[1]`, pick index 0 → job 1. +/// Result: job order `[2, 0, 1]`. +/// +/// Given a job order, start times are determined greedily (as early as possible). +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::misc::FlowShopScheduling; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// // 2 machines, 3 jobs, deadline 10 +/// let problem = FlowShopScheduling::new(2, vec![vec![2, 3], vec![3, 2], vec![1, 4]], 10); +/// let solver = BruteForce::new(); +/// let solution = solver.find_satisfying(&problem); +/// assert!(solution.is_some()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FlowShopScheduling { + /// Number of processors (machines). + num_processors: usize, + /// Task lengths: `task_lengths[j][i]` is the processing time of job `j` on machine `i`. + task_lengths: Vec>, + /// Global deadline. + deadline: u64, +} + +impl FlowShopScheduling { + /// Create a new Flow Shop Scheduling instance. + /// + /// # Arguments + /// * `num_processors` - Number of machines m + /// * `task_lengths` - task_lengths[j][i] = processing time of job j on machine i. + /// Each inner Vec must have length `num_processors`. + /// * `deadline` - Global deadline D + /// + /// # Panics + /// Panics if any job does not have exactly `num_processors` tasks. + pub fn new(num_processors: usize, task_lengths: Vec>, deadline: u64) -> Self { + for (j, tasks) in task_lengths.iter().enumerate() { + assert_eq!( + tasks.len(), + num_processors, + "Job {} has {} tasks, expected {}", + j, + tasks.len(), + num_processors + ); + } + Self { + num_processors, + task_lengths, + deadline, + } + } + + /// Get the number of processors. + pub fn num_processors(&self) -> usize { + self.num_processors + } + + /// Get the task lengths matrix. + pub fn task_lengths(&self) -> &[Vec] { + &self.task_lengths + } + + /// Get the deadline. + pub fn deadline(&self) -> u64 { + self.deadline + } + + /// Get the number of jobs. + pub fn num_jobs(&self) -> usize { + self.task_lengths.len() + } + + /// Compute the makespan for a given job ordering. + /// + /// The job_order slice must be a permutation of `0..num_jobs`. + /// Returns the completion time of the last job on the last machine. + pub fn compute_makespan(&self, job_order: &[usize]) -> u64 { + let n = job_order.len(); + let m = self.num_processors; + assert_eq!( + n, + self.task_lengths.len(), + "job_order length ({}) does not match num_jobs ({})", + n, + self.task_lengths.len() + ); + for (k, &job) in job_order.iter().enumerate() { + assert!( + job < self.task_lengths.len(), + "job_order[{}] = {} is out of range (num_jobs = {})", + k, + job, + self.task_lengths.len() + ); + } + if n == 0 || m == 0 { + return 0; + } + + // completion[k][i] = completion time of the k-th job in sequence on machine i + let mut completion = vec![vec![0u64; m]; n]; + + for (k, &job) in job_order.iter().enumerate() { + for i in 0..m { + let prev_machine = if i == 0 { 0 } else { completion[k][i - 1] }; + let prev_job = if k == 0 { 0 } else { completion[k - 1][i] }; + let start = prev_machine.max(prev_job); + completion[k][i] = start + self.task_lengths[job][i]; + } + } + + completion[n - 1][m - 1] + } +} + +impl Problem for FlowShopScheduling { + const NAME: &'static str = "FlowShopScheduling"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + let n = self.num_jobs(); + (0..n).rev().map(|i| i + 1).collect() + } + + fn evaluate(&self, config: &[usize]) -> bool { + let n = self.num_jobs(); + if config.len() != n { + return false; + } + + // 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 job_order = Vec::with_capacity(n); + for &c in config.iter() { + if c >= available.len() { + return false; + } + job_order.push(available.remove(c)); + } + + let makespan = self.compute_makespan(&job_order); + makespan <= self.deadline + } +} + +impl SatisfactionProblem for FlowShopScheduling {} + +crate::declare_variants! { + FlowShopScheduling => "factorial(num_jobs)", +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/flow_shop_scheduling.rs"] +mod tests; diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index 0df20e720..5ce862ad4 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -3,6 +3,7 @@ //! Problems with unique input structures that don't fit other categories: //! - [`BinPacking`]: Bin Packing (minimize bins) //! - [`Factoring`]: Integer factorization +//! - [`FlowShopScheduling`]: Flow Shop Scheduling (meet deadline on m processors) //! - [`Knapsack`]: 0-1 Knapsack (maximize value subject to weight capacity) //! - [`LongestCommonSubsequence`]: Longest Common Subsequence //! - [`PaintShop`]: Minimize color switches in paint shop scheduling @@ -11,6 +12,7 @@ mod bin_packing; pub(crate) mod factoring; +mod flow_shop_scheduling; mod knapsack; mod longest_common_subsequence; pub(crate) mod paintshop; @@ -19,6 +21,7 @@ mod subset_sum; pub use bin_packing::BinPacking; pub use factoring::Factoring; +pub use flow_shop_scheduling::FlowShopScheduling; pub use knapsack::Knapsack; pub use longest_common_subsequence::LongestCommonSubsequence; pub use paintshop::PaintShop; diff --git a/src/models/mod.rs b/src/models/mod.rs index 9e875fa27..de677143f 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -18,7 +18,7 @@ pub use graph::{ RuralPostman, SpinGlass, SubgraphIsomorphism, TravelingSalesman, }; pub use misc::{ - BinPacking, Factoring, Knapsack, LongestCommonSubsequence, PaintShop, + BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, PaintShop, ShortestCommonSupersequence, SubsetSum, }; pub use set::{MaximumSetPacking, MinimumSetCovering}; diff --git a/src/unit_tests/models/misc/flow_shop_scheduling.rs b/src/unit_tests/models/misc/flow_shop_scheduling.rs new file mode 100644 index 000000000..fc64940c7 --- /dev/null +++ b/src/unit_tests/models/misc/flow_shop_scheduling.rs @@ -0,0 +1,161 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::traits::Problem; + +#[test] +fn test_flow_shop_scheduling_creation() { + let problem = FlowShopScheduling::new( + 3, + vec![ + vec![3, 4, 2], + vec![2, 3, 5], + vec![4, 1, 3], + vec![1, 5, 4], + vec![3, 2, 3], + ], + 25, + ); + assert_eq!(problem.num_jobs(), 5); + assert_eq!(problem.num_processors(), 3); + assert_eq!(problem.deadline(), 25); + assert_eq!(problem.dims().len(), 5); + // Lehmer code encoding: dims = [5, 4, 3, 2, 1] + assert_eq!(problem.dims(), vec![5, 4, 3, 2, 1]); +} + +#[test] +fn test_flow_shop_scheduling_evaluate_feasible() { + // From issue example: 3 machines, 5 jobs + // Job 0: [3, 4, 2], Job 1: [2, 3, 5], Job 2: [4, 1, 3], Job 3: [1, 5, 4], Job 4: [3, 2, 3] + // Sequence j4, j1, j5, j3, j2 = jobs [3, 0, 4, 2, 1] (0-indexed) + // This has makespan 23 <= 25 + let problem = FlowShopScheduling::new( + 3, + vec![ + vec![3, 4, 2], + vec![2, 3, 5], + vec![4, 1, 3], + vec![1, 5, 4], + vec![3, 2, 3], + ], + 25, + ); + + // Lehmer code for job_order [3, 0, 4, 2, 1]: + // available=[0,1,2,3,4], pick 3 -> idx 3; available=[0,1,2,4], pick 0 -> idx 0; + // available=[1,2,4], pick 4 -> idx 2; available=[1,2], pick 2 -> idx 1; + // available=[1], pick 1 -> idx 0 + let config = vec![3, 0, 2, 1, 0]; + assert!(problem.evaluate(&config)); +} + +#[test] +fn test_flow_shop_scheduling_evaluate_infeasible() { + // Same instance, deadline of 15 (below the best makespan of 23) + let problem = FlowShopScheduling::new( + 3, + vec![ + vec![3, 4, 2], + vec![2, 3, 5], + vec![4, 1, 3], + vec![1, 5, 4], + vec![3, 2, 3], + ], + 15, // Very tight deadline, likely infeasible + ); + + // The sequence j4,j1,j5,j3,j2 gives makespan 23 > 15 + // Lehmer code for job_order [3, 0, 4, 2, 1] = [3, 0, 2, 1, 0] + let config = vec![3, 0, 2, 1, 0]; + assert!(!problem.evaluate(&config)); +} + +#[test] +fn test_flow_shop_scheduling_invalid_config() { + let problem = FlowShopScheduling::new(2, vec![vec![1, 2], vec![3, 4]], 10); + + // Lehmer code out of range: dims = [2, 1], so config[0] must be < 2, config[1] must be < 1 + assert!(!problem.evaluate(&[2, 0])); // config[0] = 2 >= 2 + assert!(!problem.evaluate(&[0, 1])); // config[1] = 1 >= 1 + // Wrong length + assert!(!problem.evaluate(&[0])); + assert!(!problem.evaluate(&[0, 0, 0])); +} + +#[test] +fn test_flow_shop_scheduling_problem_name() { + assert_eq!(::NAME, "FlowShopScheduling"); +} + +#[test] +fn test_flow_shop_scheduling_variant() { + let v = ::variant(); + assert!(v.is_empty()); +} + +#[test] +fn test_flow_shop_scheduling_serialization() { + let problem = FlowShopScheduling::new(2, vec![vec![1, 2], vec![3, 4], vec![2, 1]], 10); + let json = serde_json::to_value(&problem).unwrap(); + let restored: FlowShopScheduling = serde_json::from_value(json).unwrap(); + assert_eq!(restored.num_processors(), problem.num_processors()); + assert_eq!(restored.task_lengths(), problem.task_lengths()); + assert_eq!(restored.deadline(), problem.deadline()); +} + +#[test] +fn test_flow_shop_scheduling_compute_makespan() { + // 2 machines, 3 jobs + // Job 0: [3, 2], Job 1: [2, 4], Job 2: [1, 3] + let problem = FlowShopScheduling::new(2, vec![vec![3, 2], vec![2, 4], vec![1, 3]], 20); + + // Order: job 0, job 1, job 2 + // Machine 0: j0[0,3], j1[3,5], j2[5,6] + // Machine 1: j0[3,5], j1[5,9], j2[9,12] + // Makespan = 12 + assert_eq!(problem.compute_makespan(&[0, 1, 2]), 12); +} + +#[test] +fn test_flow_shop_scheduling_brute_force_solver() { + // Small instance: 2 machines, 3 jobs, generous deadline + let problem = FlowShopScheduling::new(2, vec![vec![3, 2], vec![2, 4], vec![1, 3]], 20); + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!(solution.is_some()); + let config = solution.unwrap(); + assert!(problem.evaluate(&config)); +} + +#[test] +fn test_flow_shop_scheduling_brute_force_unsatisfiable() { + // 2 machines, 2 jobs with impossible deadline + // Job 0: [5, 5], Job 1: [5, 5] + // Best makespan: min of two orders: + // [0,1]: M0: 0-5, 5-10; M1: 5-10, 10-15 -> 15 + // [1,0]: same by symmetry -> 15 + // Deadline 10 < 15 => unsatisfiable + let problem = FlowShopScheduling::new(2, vec![vec![5, 5], vec![5, 5]], 10); + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!(solution.is_none()); +} + +#[test] +fn test_flow_shop_scheduling_empty() { + let problem = FlowShopScheduling::new(3, vec![], 0); + assert_eq!(problem.num_jobs(), 0); + assert_eq!(problem.dims(), Vec::::new()); + // Empty config should be satisfying (no jobs to schedule) + assert!(problem.evaluate(&[])); +} + +#[test] +fn test_flow_shop_scheduling_single_job() { + // 3 machines, 1 job: [2, 3, 4] + // Makespan = 2 + 3 + 4 = 9 + let problem = FlowShopScheduling::new(3, vec![vec![2, 3, 4]], 10); + assert!(problem.evaluate(&[0])); // makespan 9 <= 10 + let tight = FlowShopScheduling::new(3, vec![vec![2, 3, 4]], 8); + assert!(!tight.evaluate(&[0])); // makespan 9 > 8 +} diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index 5c6fe34bb..e0bf2d716 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -16,8 +16,8 @@ fn check_problem_trait(problem: &P, name: &str) { ); for d in &dims { assert!( - *d >= 2, - "{} should have at least 2 choices per dimension", + *d >= 1, + "{} should have at least 1 choice per dimension", name ); } @@ -102,6 +102,10 @@ fn test_all_problems_implement_trait_correctly() { &ShortestCommonSupersequence::new(2, vec![vec![0, 1], vec![1, 0]], 3), "ShortestCommonSupersequence", ); + check_problem_trait( + &FlowShopScheduling::new(2, vec![vec![1, 2], vec![3, 4]], 10), + "FlowShopScheduling", + ); } #[test]