From 043edd90155e515cd4efc4d861ed17e09dc70c97 Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 08:38:05 +0000 Subject: [PATCH 1/8] Add plan for #507: FlowShopScheduling model --- .../2026-03-13-flowshopscheduling-model.md | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 docs/plans/2026-03-13-flowshopscheduling-model.md diff --git a/docs/plans/2026-03-13-flowshopscheduling-model.md b/docs/plans/2026-03-13-flowshopscheduling-model.md new file mode 100644 index 000000000..75e127583 --- /dev/null +++ b/docs/plans/2026-03-13-flowshopscheduling-model.md @@ -0,0 +1,81 @@ +# Plan: Add FlowShopScheduling Model (#507) + +## Summary +Add the `FlowShopScheduling` satisfaction problem model — a classic NP-complete scheduling problem from Garey & Johnson (A5 SS15). Given m processors and n jobs (each with m tasks in fixed processor order), determine if all jobs can complete by deadline D. + +## Information Checklist + +| # | Item | Value | +|---|------|-------| +| 1 | Problem name | `FlowShopScheduling` | +| 2 | Mathematical definition | Given m processors, n jobs each with m tasks of specified lengths, and deadline D, determine if a flow-shop schedule exists meeting D | +| 3 | Problem type | Satisfaction (`Metric = bool`) | +| 4 | Type parameters | None | +| 5 | Struct fields | `num_processors: usize`, `task_lengths: Vec>`, `deadline: u64` | +| 6 | Configuration space | `vec![num_jobs; num_jobs]` — each variable is a permutation position {0..n-1} | +| 7 | Feasibility check | Config must be a valid permutation of jobs; then compute schedule greedily and check makespan <= deadline | +| 8 | Objective function | `bool` — true iff valid permutation schedule meets deadline | +| 9 | Best known exact | For general m: `num_jobs! * num_processors * num_jobs` (brute-force over permutations). Use `"num_jobs! * num_processors * num_jobs"` but since factorial isn't supported in Expr, use a conservative bound. The issue mentions O*(3^n) for m=3 specifically. For general case, use brute-force enumeration. | +| 10 | Solving strategy | BruteForce (enumerate all permutation configs, check feasibility) | +| 11 | Category | `misc` (unique scheduling input structure) | + +## Steps + +### Step 1: Create model file `src/models/misc/flow_shop_scheduling.rs` + +- `FlowShopScheduling` struct with fields: `num_processors`, `task_lengths`, `deadline` +- Constructor `new(num_processors, task_lengths, deadline)` with validation (each job must have exactly m tasks) +- Getters: `num_processors()`, `task_lengths()`, `deadline()`, `num_jobs()` +- `compute_makespan(job_order: &[usize]) -> u64` helper that computes the flow-shop makespan given a job sequence +- `Problem` impl with `Metric = bool`: + - `NAME = "FlowShopScheduling"` + - `dims()` returns `vec![num_jobs; num_jobs]` (permutation encoding) + - `evaluate()`: validate config is a permutation, compute makespan, return makespan <= deadline + - `variant()` returns `crate::variant_params![]` (no type params) +- `SatisfactionProblem` marker impl +- `declare_variants!` with complexity `"3^num_jobs"` (conservative bound from the O*(3^n) DP for m=3) +- `inventory::submit!` for `ProblemSchemaEntry` +- `#[cfg(test)] #[path]` link to unit tests + +### Step 2: Register the model + +- `src/models/misc/mod.rs`: add `mod flow_shop_scheduling;` and `pub use flow_shop_scheduling::FlowShopScheduling;` +- `src/models/mod.rs`: add `FlowShopScheduling` to the `pub use misc::` line +- `src/lib.rs`: add `FlowShopScheduling` to the prelude re-export + +### Step 3: Register in CLI + +- `problemreductions-cli/src/dispatch.rs`: + - `load_problem()`: add `"FlowShopScheduling" => deser_sat::(data)` + - `serialize_any_problem()`: add `"FlowShopScheduling" => try_ser::(any)` + - Add import for `FlowShopScheduling` +- `problemreductions-cli/src/problem_name.rs`: + - Add `"flowshopscheduling" => "FlowShopScheduling".to_string()` to `resolve_alias()` +- `problemreductions-cli/src/commands/create.rs`: + - Add creation handler for FlowShopScheduling using `--task-lengths` (semicolon-separated rows) and `--deadline` + - Note: `--m` flag already exists (for Factoring), can reuse for num_processors +- `problemreductions-cli/src/cli.rs`: + - Add `--task-lengths` and `--deadline` flags to `CreateArgs` + - Update `all_data_flags_empty()` to check new flags + - Update help table with FlowShopScheduling entry + +### Step 4: Write unit tests `src/unit_tests/models/misc/flow_shop_scheduling.rs` + +Tests to write: +- `test_flow_shop_scheduling_creation` — construct instance, verify dimensions +- `test_flow_shop_scheduling_evaluate_feasible` — use the issue's example (sequence j4,j1,j5,j3,j2 with makespan 23 <= 25) +- `test_flow_shop_scheduling_evaluate_infeasible` — a sequence exceeding deadline +- `test_flow_shop_scheduling_invalid_config` — non-permutation config returns false +- `test_flow_shop_scheduling_direction` — verify it's a satisfaction problem +- `test_flow_shop_scheduling_serialization` — round-trip serde +- `test_flow_shop_scheduling_solver` — brute force finds a satisfying assignment for small instance +- `test_flow_shop_scheduling_empty` — 0 jobs case +- `test_flow_shop_scheduling_variant` — verify empty variant + +Also add `mod flow_shop_scheduling;` to `src/unit_tests/models/misc/mod.rs` (create if needed). + +### Step 5: Verify + +- `make test` — all tests pass +- `make clippy` — no warnings +- `make fmt` — code formatted From 5239ba23dbef55ccfb650d939c888315bb85d92e Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 08:45:39 +0000 Subject: [PATCH 2/8] Implement #507: Add FlowShopScheduling model Co-Authored-By: Claude Opus 4.6 --- problemreductions-cli/src/cli.rs | 7 + problemreductions-cli/src/commands/create.rs | 35 +++- problemreductions-cli/src/dispatch.rs | 4 +- problemreductions-cli/src/problem_name.rs | 1 + src/lib.rs | 4 +- src/models/misc/flow_shop_scheduling.rs | 184 ++++++++++++++++++ src/models/misc/mod.rs | 3 + src/models/mod.rs | 2 +- .../models/misc/flow_shop_scheduling.rs | 161 +++++++++++++++ 9 files changed, 397 insertions(+), 4 deletions(-) create mode 100644 src/models/misc/flow_shop_scheduling.rs create mode 100644 src/unit_tests/models/misc/flow_shop_scheduling.rs diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 91792e20b..30b61aa6c 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -218,6 +218,7 @@ Flags by problem type: BMF --matrix (0/1), --rank CVP --basis, --target-vec [--bounds] FVS --arcs [--weights] [--num-vertices] + FlowShopScheduling --task-lengths, --deadline [--m] ILP, CircuitSAT (via reduction only) Geometry graph variants (use slash notation, e.g., MIS/KingsSubgraph): @@ -332,6 +333,12 @@ 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, } #[derive(clap::Args)] diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 2df4f0994..cdc9ebbac 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -6,7 +6,7 @@ use crate::util; use anyhow::{bail, Context, Result}; use problemreductions::models::algebraic::{ClosestVectorProblem, BMF}; use problemreductions::models::graph::GraphPartitioning; -use problemreductions::models::misc::{BinPacking, PaintShop}; +use problemreductions::models::misc::{BinPacking, FlowShopScheduling, PaintShop}; use problemreductions::prelude::*; use problemreductions::registry::collect_schemas; use problemreductions::topology::{ @@ -48,6 +48,8 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.target_vec.is_none() && args.bounds.is_none() && args.arcs.is_none() + && args.task_lengths.is_none() + && args.deadline.is_none() } fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { @@ -459,6 +461,37 @@ 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 --m 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 --m 3" + ) + })?; + let task_lengths: Vec> = task_str + .split(';') + .map(|row| util::parse_comma_list(row.trim())) + .collect::>>()?; + let num_processors = 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 --m"); + }; + ( + ser(FlowShopScheduling::new(num_processors, task_lengths, deadline))?, + 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 49dd523cb..d012552fe 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, FlowShopScheduling, Knapsack, 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), + "FlowShopScheduling" => 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), + "FlowShopScheduling" => 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 2b6c8c737..701f1458b 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(), + "flowshopscheduling" => "FlowShopScheduling".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 a74c906f3..6a30b2821 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,7 +45,9 @@ pub mod prelude { KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumFeedbackVertexSet, MinimumVertexCover, TravelingSalesman, }; - pub use crate::models::misc::{BinPacking, Factoring, Knapsack, PaintShop, SubsetSum}; + pub use crate::models::misc::{ + BinPacking, Factoring, FlowShopScheduling, Knapsack, PaintShop, SubsetSum, + }; pub use crate::models::set::{MaximumSetPacking, MinimumSetCovering}; // Core traits diff --git a/src/models/misc/flow_shop_scheduling.rs b/src/models/misc/flow_shop_scheduling.rs new file mode 100644 index 000000000..8c0f86f69 --- /dev/null +++ b/src/models/misc/flow_shop_scheduling.rs @@ -0,0 +1,184 @@ +//! 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 +/// +/// Each variable represents a job's position in the processing sequence. +/// A valid configuration is a permutation of `{0, ..., n-1}`. +/// Given a permutation, 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; + 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(); + vec![n; n] + } + + fn evaluate(&self, config: &[usize]) -> bool { + let n = self.num_jobs(); + if config.len() != n { + return false; + } + + // Check that config is a valid permutation of 0..n + let mut seen = vec![false; n]; + for &pos in config { + if pos >= n || seen[pos] { + return false; + } + seen[pos] = true; + } + + // config[j] = position of job j in the sequence + // We need to convert to job_order: job_order[position] = job + let mut job_order = vec![0usize; n]; + for (job, &pos) in config.iter().enumerate() { + job_order[pos] = job; + } + + let makespan = self.compute_makespan(&job_order); + makespan <= self.deadline + } +} + +impl SatisfactionProblem for FlowShopScheduling {} + +crate::declare_variants! { + FlowShopScheduling => "3^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 36ebe905b..ef0495a6a 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -3,18 +3,21 @@ //! 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) //! - [`PaintShop`]: Minimize color switches in paint shop scheduling //! - [`SubsetSum`]: Find a subset summing to exactly a target value mod bin_packing; pub(crate) mod factoring; +mod flow_shop_scheduling; mod knapsack; pub(crate) mod paintshop; mod subset_sum; pub use bin_packing::BinPacking; pub use factoring::Factoring; +pub use flow_shop_scheduling::FlowShopScheduling; pub use knapsack::Knapsack; pub use paintshop::PaintShop; pub use subset_sum::SubsetSum; diff --git a/src/models/mod.rs b/src/models/mod.rs index ceb584ce2..7e48afeb2 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, FlowShopScheduling, Knapsack, PaintShop, 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..f3041f2da --- /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); + // Each variable has domain {0, ..., 4} + assert!(problem.dims().iter().all(|&d| d == 5)); +} + +#[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, + ); + + // config[job] = position in sequence + // job_order = [3, 0, 4, 2, 1] means: + // position 0 -> job 3, position 1 -> job 0, position 2 -> job 4, position 3 -> job 2, position 4 -> job 1 + // So config: job 0 -> pos 1, job 1 -> pos 4, job 2 -> pos 3, job 3 -> pos 0, job 4 -> pos 2 + let config = vec![1, 4, 3, 0, 2]; + assert!(problem.evaluate(&config)); +} + +#[test] +fn test_flow_shop_scheduling_evaluate_infeasible() { + // Same instance, tight deadline of 22 (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 + let config = vec![1, 4, 3, 0, 2]; + assert!(!problem.evaluate(&config)); +} + +#[test] +fn test_flow_shop_scheduling_invalid_config_not_permutation() { + let problem = FlowShopScheduling::new(2, vec![vec![1, 2], vec![3, 4]], 10); + + // Duplicate position + assert!(!problem.evaluate(&[0, 0])); + // Out of range + assert!(!problem.evaluate(&[0, 2])); + // Wrong length + assert!(!problem.evaluate(&[0])); + assert!(!problem.evaluate(&[0, 1, 2])); +} + +#[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 +} From a1aebc4c4af331ef968a4f60b1ec3a0711fc683c Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 08:45:42 +0000 Subject: [PATCH 3/8] chore: remove plan file after implementation --- .../2026-03-13-flowshopscheduling-model.md | 81 ------------------- 1 file changed, 81 deletions(-) delete mode 100644 docs/plans/2026-03-13-flowshopscheduling-model.md diff --git a/docs/plans/2026-03-13-flowshopscheduling-model.md b/docs/plans/2026-03-13-flowshopscheduling-model.md deleted file mode 100644 index 75e127583..000000000 --- a/docs/plans/2026-03-13-flowshopscheduling-model.md +++ /dev/null @@ -1,81 +0,0 @@ -# Plan: Add FlowShopScheduling Model (#507) - -## Summary -Add the `FlowShopScheduling` satisfaction problem model — a classic NP-complete scheduling problem from Garey & Johnson (A5 SS15). Given m processors and n jobs (each with m tasks in fixed processor order), determine if all jobs can complete by deadline D. - -## Information Checklist - -| # | Item | Value | -|---|------|-------| -| 1 | Problem name | `FlowShopScheduling` | -| 2 | Mathematical definition | Given m processors, n jobs each with m tasks of specified lengths, and deadline D, determine if a flow-shop schedule exists meeting D | -| 3 | Problem type | Satisfaction (`Metric = bool`) | -| 4 | Type parameters | None | -| 5 | Struct fields | `num_processors: usize`, `task_lengths: Vec>`, `deadline: u64` | -| 6 | Configuration space | `vec![num_jobs; num_jobs]` — each variable is a permutation position {0..n-1} | -| 7 | Feasibility check | Config must be a valid permutation of jobs; then compute schedule greedily and check makespan <= deadline | -| 8 | Objective function | `bool` — true iff valid permutation schedule meets deadline | -| 9 | Best known exact | For general m: `num_jobs! * num_processors * num_jobs` (brute-force over permutations). Use `"num_jobs! * num_processors * num_jobs"` but since factorial isn't supported in Expr, use a conservative bound. The issue mentions O*(3^n) for m=3 specifically. For general case, use brute-force enumeration. | -| 10 | Solving strategy | BruteForce (enumerate all permutation configs, check feasibility) | -| 11 | Category | `misc` (unique scheduling input structure) | - -## Steps - -### Step 1: Create model file `src/models/misc/flow_shop_scheduling.rs` - -- `FlowShopScheduling` struct with fields: `num_processors`, `task_lengths`, `deadline` -- Constructor `new(num_processors, task_lengths, deadline)` with validation (each job must have exactly m tasks) -- Getters: `num_processors()`, `task_lengths()`, `deadline()`, `num_jobs()` -- `compute_makespan(job_order: &[usize]) -> u64` helper that computes the flow-shop makespan given a job sequence -- `Problem` impl with `Metric = bool`: - - `NAME = "FlowShopScheduling"` - - `dims()` returns `vec![num_jobs; num_jobs]` (permutation encoding) - - `evaluate()`: validate config is a permutation, compute makespan, return makespan <= deadline - - `variant()` returns `crate::variant_params![]` (no type params) -- `SatisfactionProblem` marker impl -- `declare_variants!` with complexity `"3^num_jobs"` (conservative bound from the O*(3^n) DP for m=3) -- `inventory::submit!` for `ProblemSchemaEntry` -- `#[cfg(test)] #[path]` link to unit tests - -### Step 2: Register the model - -- `src/models/misc/mod.rs`: add `mod flow_shop_scheduling;` and `pub use flow_shop_scheduling::FlowShopScheduling;` -- `src/models/mod.rs`: add `FlowShopScheduling` to the `pub use misc::` line -- `src/lib.rs`: add `FlowShopScheduling` to the prelude re-export - -### Step 3: Register in CLI - -- `problemreductions-cli/src/dispatch.rs`: - - `load_problem()`: add `"FlowShopScheduling" => deser_sat::(data)` - - `serialize_any_problem()`: add `"FlowShopScheduling" => try_ser::(any)` - - Add import for `FlowShopScheduling` -- `problemreductions-cli/src/problem_name.rs`: - - Add `"flowshopscheduling" => "FlowShopScheduling".to_string()` to `resolve_alias()` -- `problemreductions-cli/src/commands/create.rs`: - - Add creation handler for FlowShopScheduling using `--task-lengths` (semicolon-separated rows) and `--deadline` - - Note: `--m` flag already exists (for Factoring), can reuse for num_processors -- `problemreductions-cli/src/cli.rs`: - - Add `--task-lengths` and `--deadline` flags to `CreateArgs` - - Update `all_data_flags_empty()` to check new flags - - Update help table with FlowShopScheduling entry - -### Step 4: Write unit tests `src/unit_tests/models/misc/flow_shop_scheduling.rs` - -Tests to write: -- `test_flow_shop_scheduling_creation` — construct instance, verify dimensions -- `test_flow_shop_scheduling_evaluate_feasible` — use the issue's example (sequence j4,j1,j5,j3,j2 with makespan 23 <= 25) -- `test_flow_shop_scheduling_evaluate_infeasible` — a sequence exceeding deadline -- `test_flow_shop_scheduling_invalid_config` — non-permutation config returns false -- `test_flow_shop_scheduling_direction` — verify it's a satisfaction problem -- `test_flow_shop_scheduling_serialization` — round-trip serde -- `test_flow_shop_scheduling_solver` — brute force finds a satisfying assignment for small instance -- `test_flow_shop_scheduling_empty` — 0 jobs case -- `test_flow_shop_scheduling_variant` — verify empty variant - -Also add `mod flow_shop_scheduling;` to `src/unit_tests/models/misc/mod.rs` (create if needed). - -### Step 5: Verify - -- `make test` — all tests pass -- `make clippy` — no warnings -- `make fmt` — code formatted From 15225118253cfcf4f2896664e3d53f9f8cb46829 Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 15:24:04 +0000 Subject: [PATCH 4/8] fix: address Copilot review comments on FlowShopScheduling - Switch dims() from vec![n;n] to Lehmer code encoding [n,n-1,...,1] - Update evaluate() to decode Lehmer code into permutations - Fix complexity from "3^num_jobs" to "num_jobs^num_jobs" - Add validation to compute_makespan() for out-of-bounds indices - Add --num-processors CLI flag instead of overloading --m - Validate task_lengths row lengths in CLI with bail! instead of panic - Fix test comment about deadline value Co-Authored-By: Claude Opus 4.6 --- problemreductions-cli/src/cli.rs | 5 ++- problemreductions-cli/src/commands/create.rs | 21 ++++++++-- src/models/misc/flow_shop_scheduling.rs | 39 ++++++++++++------- .../models/misc/flow_shop_scheduling.rs | 30 +++++++------- 4 files changed, 61 insertions(+), 34 deletions(-) diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 166d41e2a..d710b1b9d 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -223,7 +223,7 @@ Flags by problem type: SubgraphIsomorphism --graph (host), --pattern (pattern) LCS --strings FVS --arcs [--weights] [--num-vertices] - FlowShopScheduling --task-lengths, --deadline [--m] + FlowShopScheduling --task-lengths, --deadline [--num-processors] ILP, CircuitSAT (via reduction only) Geometry graph variants (use slash notation, e.g., MIS/KingsSubgraph): @@ -356,6 +356,9 @@ pub struct CreateArgs { /// Deadline for FlowShopScheduling #[arg(long)] pub deadline: Option, + /// Number of processors/machines for FlowShopScheduling + #[arg(long)] + pub num_processors: Option, } #[derive(clap::Args)] diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index f8cf4d4d2..1a9085b68 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -54,6 +54,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.arcs.is_none() && args.task_lengths.is_none() && args.deadline.is_none() + && args.num_processors.is_none() } fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { @@ -549,26 +550,38 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { 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 --m 3" + 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 --m 3" + 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(m) = args.m { + 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 --m"); + 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(), diff --git a/src/models/misc/flow_shop_scheduling.rs b/src/models/misc/flow_shop_scheduling.rs index 8c0f86f69..7c9a13f23 100644 --- a/src/models/misc/flow_shop_scheduling.rs +++ b/src/models/misc/flow_shop_scheduling.rs @@ -113,6 +113,22 @@ impl FlowShopScheduling { 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; } @@ -143,7 +159,7 @@ impl Problem for FlowShopScheduling { fn dims(&self) -> Vec { let n = self.num_jobs(); - vec![n; n] + (0..n).rev().map(|i| i + 1).collect() } fn evaluate(&self, config: &[usize]) -> bool { @@ -152,20 +168,15 @@ impl Problem for FlowShopScheduling { return false; } - // Check that config is a valid permutation of 0..n - let mut seen = vec![false; n]; - for &pos in config { - if pos >= n || 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 job_order = Vec::with_capacity(n); + for (_i, &c) in config.iter().enumerate() { + if c >= available.len() { return false; } - seen[pos] = true; - } - - // config[j] = position of job j in the sequence - // We need to convert to job_order: job_order[position] = job - let mut job_order = vec![0usize; n]; - for (job, &pos) in config.iter().enumerate() { - job_order[pos] = job; + job_order.push(available.remove(c)); } let makespan = self.compute_makespan(&job_order); @@ -176,7 +187,7 @@ impl Problem for FlowShopScheduling { impl SatisfactionProblem for FlowShopScheduling {} crate::declare_variants! { - FlowShopScheduling => "3^num_jobs", + FlowShopScheduling => "num_jobs^num_jobs", } #[cfg(test)] diff --git a/src/unit_tests/models/misc/flow_shop_scheduling.rs b/src/unit_tests/models/misc/flow_shop_scheduling.rs index f3041f2da..fc64940c7 100644 --- a/src/unit_tests/models/misc/flow_shop_scheduling.rs +++ b/src/unit_tests/models/misc/flow_shop_scheduling.rs @@ -19,8 +19,8 @@ fn test_flow_shop_scheduling_creation() { assert_eq!(problem.num_processors(), 3); assert_eq!(problem.deadline(), 25); assert_eq!(problem.dims().len(), 5); - // Each variable has domain {0, ..., 4} - assert!(problem.dims().iter().all(|&d| d == 5)); + // Lehmer code encoding: dims = [5, 4, 3, 2, 1] + assert_eq!(problem.dims(), vec![5, 4, 3, 2, 1]); } #[test] @@ -41,17 +41,17 @@ fn test_flow_shop_scheduling_evaluate_feasible() { 25, ); - // config[job] = position in sequence - // job_order = [3, 0, 4, 2, 1] means: - // position 0 -> job 3, position 1 -> job 0, position 2 -> job 4, position 3 -> job 2, position 4 -> job 1 - // So config: job 0 -> pos 1, job 1 -> pos 4, job 2 -> pos 3, job 3 -> pos 0, job 4 -> pos 2 - let config = vec![1, 4, 3, 0, 2]; + // 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, tight deadline of 22 (below the best makespan of 23) + // Same instance, deadline of 15 (below the best makespan of 23) let problem = FlowShopScheduling::new( 3, vec![ @@ -65,21 +65,21 @@ fn test_flow_shop_scheduling_evaluate_infeasible() { ); // The sequence j4,j1,j5,j3,j2 gives makespan 23 > 15 - let config = vec![1, 4, 3, 0, 2]; + // 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_not_permutation() { +fn test_flow_shop_scheduling_invalid_config() { let problem = FlowShopScheduling::new(2, vec![vec![1, 2], vec![3, 4]], 10); - // Duplicate position - assert!(!problem.evaluate(&[0, 0])); - // Out of range - assert!(!problem.evaluate(&[0, 2])); + // 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, 1, 2])); + assert!(!problem.evaluate(&[0, 0, 0])); } #[test] From 52e8ca0c95562b76e8e6804699fff62a31a15eeb Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 15:33:11 +0000 Subject: [PATCH 5/8] docs: add Lehmer code encoding explanation to FlowShopScheduling Add concrete example of how Lehmer code configs map to job orderings in the struct doc comment, improving discoverability for new users. Co-Authored-By: Claude Opus 4.6 --- src/models/misc/flow_shop_scheduling.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/models/misc/flow_shop_scheduling.rs b/src/models/misc/flow_shop_scheduling.rs index 7c9a13f23..509af9686 100644 --- a/src/models/misc/flow_shop_scheduling.rs +++ b/src/models/misc/flow_shop_scheduling.rs @@ -31,9 +31,15 @@ inventory::submit! { /// /// # Representation /// -/// Each variable represents a job's position in the processing sequence. -/// A valid configuration is a permutation of `{0, ..., n-1}`. -/// Given a permutation, start times are determined greedily (as early as possible). +/// 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 /// From 03ce50feda4d0f8a5dcd57dddf79d4b63a31432f Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 15:37:11 +0000 Subject: [PATCH 6/8] fix: remove unused enumerate in Lehmer code decoding Fixes clippy::unused_enumerate_index warning. Co-Authored-By: Claude Opus 4.6 --- src/models/misc/flow_shop_scheduling.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/misc/flow_shop_scheduling.rs b/src/models/misc/flow_shop_scheduling.rs index 509af9686..bf5c8c221 100644 --- a/src/models/misc/flow_shop_scheduling.rs +++ b/src/models/misc/flow_shop_scheduling.rs @@ -178,7 +178,7 @@ impl Problem for FlowShopScheduling { // 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 (_i, &c) in config.iter().enumerate() { + for &c in config.iter() { if c >= available.len() { return false; } From 0f1fb53d1792a9cb91013bc98e6f61bb958e2868 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 14 Mar 2026 01:51:48 +0800 Subject: [PATCH 7/8] fix: add missing paper section, trait_consistency entry, and correct complexity - Add FlowShopScheduling problem-def and display-name to docs/paper/reductions.typ - Add FlowShopScheduling to trait_consistency test - Fix complexity from "num_jobs^num_jobs" to "factorial(num_jobs)" (n! is the exact search space) - Relax check_problem_trait assertion from >= 2 to >= 1 (Lehmer code encoding produces trailing dim of 1) Co-Authored-By: Claude Opus 4.6 --- docs/paper/reductions.typ | 9 +++++++++ src/models/misc/flow_shop_scheduling.rs | 2 +- src/unit_tests/trait_consistency.rs | 8 ++++++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 8298a429c..db29592fe 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -62,6 +62,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 @@ -1046,6 +1047,14 @@ 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 = 2$, $n = 3$ jobs with task lengths $ell = mat(3, 2; 2, 4; 1, 3)$ (rows = jobs, columns = machines), and $D = 12$. For the job order $(0, 1, 2)$: Machine 1 processes jobs at $[0,3], [3,5], [5,6]$; Machine 2 at $[3,5], [5,9], [9,12]$. Makespan $= 12 <= D$, so the schedule is feasible. +] + // Completeness check: warn about problem types in JSON but missing from paper #{ let json-models = { diff --git a/src/models/misc/flow_shop_scheduling.rs b/src/models/misc/flow_shop_scheduling.rs index bf5c8c221..6568cced8 100644 --- a/src/models/misc/flow_shop_scheduling.rs +++ b/src/models/misc/flow_shop_scheduling.rs @@ -193,7 +193,7 @@ impl Problem for FlowShopScheduling { impl SatisfactionProblem for FlowShopScheduling {} crate::declare_variants! { - FlowShopScheduling => "num_jobs^num_jobs", + FlowShopScheduling => "factorial(num_jobs)", } #[cfg(test)] diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index 91754a81d..e55f59fed 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 ); } @@ -98,6 +98,10 @@ fn test_all_problems_implement_trait_correctly() { &HamiltonianPath::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])), "HamiltonianPath", ); + check_problem_trait( + &FlowShopScheduling::new(2, vec![vec![1, 2], vec![3, 4]], 10), + "FlowShopScheduling", + ); } #[test] From 2663f064d7e53cd1552342857ab9df0247a4fd41 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 14 Mar 2026 01:54:35 +0800 Subject: [PATCH 8/8] docs: add Gantt chart visualization and use issue's 5-job example in paper Replace the simple 2-machine example with the issue's canonical 3-machine, 5-job example. Add a CeTZ Gantt chart showing the feasible schedule with job order (j4, j1, j5, j3, j2) achieving makespan 23 within deadline 25. Co-Authored-By: Claude Opus 4.6 --- docs/paper/reductions.typ | 65 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index db29592fe..d93d2288d 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -1052,7 +1052,70 @@ Biclique Cover is equivalent to factoring the biadjacency matrix $M$ of the bipa ][ 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 = 2$, $n = 3$ jobs with task lengths $ell = mat(3, 2; 2, 4; 1, 3)$ (rows = jobs, columns = machines), and $D = 12$. For the job order $(0, 1, 2)$: Machine 1 processes jobs at $[0,3], [3,5], [5,6]$; Machine 2 at $[3,5], [5,9], [9,12]$. Makespan $= 12 <= D$, so the schedule is feasible. + *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