From 09547941215030aadea03e158fbb54f283bc5158 Mon Sep 17 00:00:00 2001 From: zazabap Date: Tue, 10 Mar 2026 14:26:34 +0000 Subject: [PATCH 1/6] Add plan for #212: [Model] MultiprocessorScheduling Co-Authored-By: Claude Opus 4.6 --- .../2026-03-10-multiprocessor-scheduling.md | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 docs/plans/2026-03-10-multiprocessor-scheduling.md diff --git a/docs/plans/2026-03-10-multiprocessor-scheduling.md b/docs/plans/2026-03-10-multiprocessor-scheduling.md new file mode 100644 index 000000000..fb22490f7 --- /dev/null +++ b/docs/plans/2026-03-10-multiprocessor-scheduling.md @@ -0,0 +1,147 @@ +# Plan: [Model] MultiprocessorScheduling (#212) + +## Problem Summary + +**Name:** `MultiprocessorScheduling` +**Reference:** Garey & Johnson, *Computers and Intractability*, A5 SS8 +**Category:** `misc/` (scheduling input: list of processing times + number of machines) + +### Mathematical Definition + +INSTANCE: Set T of tasks, number m of processors, length l(t) for each t in T, and a deadline D. +QUESTION: Is there an assignment of tasks to processors such that the total load on each processor does not exceed D? + +Equivalently: given n jobs with integer processing times and m identical parallel machines, assign each job to a machine such that for every machine i, the sum of processing times of jobs assigned to i is at most D. + +### Problem Type + +**Satisfaction problem** (`Metric = bool`, implements `SatisfactionProblem`). + +The issue defines this as a decision problem: "Is there an m-processor schedule for T that meets the overall deadline D?" A configuration is feasible (true) iff for every processor, the total load does not exceed D. + +### Variables + +- **Count:** n = |T| (one variable per task) +- **Per-variable domain:** {0, 1, ..., m-1} -- the processor index assigned to the task +- **dims():** `vec![num_processors; num_tasks]` + +### Evaluation + +``` +evaluate(config): + for each processor i in 0..m: + load_i = sum of lengths[j] for all j where config[j] == i + if load_i > deadline: return false + return true +``` + +### Struct Fields + +| Field | Type | Description | +|------------------|------------|-------------------------------------| +| `lengths` | `Vec` | Processing time l(t) for each task | +| `num_processors` | `u64` | Number of identical processors m | +| `deadline` | `u64` | Global deadline D | + +### Getter Methods (for overhead system) + +- `num_tasks() -> usize` -- returns `lengths.len()` +- `num_processors() -> u64` -- returns `self.num_processors` + +### Complexity + +- For general m (part of input): strongly NP-hard. No known improvement over O*(m^n) brute-force enumeration. +- For fixed m: weakly NP-hard, solvable by pseudo-polynomial DP in O(n * D^(m-1)). +- Complexity string: `"num_processors ^ num_tasks"` (general case brute force) +- References: Garey & Johnson 1979; Lenstra, Rinnooy Kan & Brucker, *Annals of Discrete Mathematics*, 1977. + +### Solving Strategy + +- BruteForce: enumerate all m^n assignments, check if max load <= D. +- ILP: binary variables x_{t,i}, constraints sum_i x_{t,i} = 1, sum_t x_{t,i} * l(t) <= D. + +### Example Instance + +T = {t1, t2, t3, t4, t5}, lengths = [4, 5, 3, 2, 6], m = 2, D = 10. +Feasible assignment: config = [0, 1, 1, 1, 0] (processor 0 gets {t1, t5} load=10, processor 1 gets {t2, t3, t4} load=10). +Answer: true. + +--- + +## Implementation Steps + +### Step 1: Category + +`misc/` -- scheduling input does not fit graph, formula, set, or algebraic categories. + +### Step 2: Implement the model + +Create `src/models/misc/multiprocessor_scheduling.rs`: + +1. `inventory::submit!` for `ProblemSchemaEntry` with fields: `lengths`, `num_processors`, `deadline` +2. Struct `MultiprocessorScheduling` with `#[derive(Debug, Clone, Serialize, Deserialize)]` +3. Constructor `new(lengths: Vec, num_processors: u64, deadline: u64) -> Self` +4. Accessors: `lengths()`, `num_processors()`, `deadline()`, `num_tasks()` +5. `Problem` impl: + - `NAME = "MultiprocessorScheduling"` + - `Metric = bool` + - `variant() -> crate::variant_params![]` (no type parameters) + - `dims() -> vec![self.num_processors as usize; self.num_tasks()]` + - `evaluate()`: compute load per processor, return true iff all loads <= deadline +6. `SatisfactionProblem` impl (marker trait) +7. `declare_variants! { MultiprocessorScheduling => "num_processors ^ num_tasks" }` +8. `#[cfg(test)] #[path = "..."] mod tests;` + +### Step 2.5: Register variant complexity + +```rust +crate::declare_variants! { + MultiprocessorScheduling => "num_processors ^ num_tasks", +} +``` + +### Step 3: Register the model + +1. `src/models/misc/mod.rs`: add `mod multiprocessor_scheduling;` and `pub use multiprocessor_scheduling::MultiprocessorScheduling;` +2. `src/models/mod.rs`: add to the misc re-export line + +### Step 4: Register in CLI + +1. `problemreductions-cli/src/dispatch.rs`: + - `load_problem()`: add match arm `"MultiprocessorScheduling" => deser_sat::(json)` + - `serialize_any_problem()`: add match arm `"MultiprocessorScheduling" => try_ser::(json)` +2. `problemreductions-cli/src/problem_name.rs`: + - `resolve_alias()`: add `"multiprocessorscheduling" => "MultiprocessorScheduling".to_string()` + - No short alias (no well-established abbreviation in the literature) + +### Step 4.5: Add CLI creation support + +Add match arm in `problemreductions-cli/src/commands/create.rs` for `"MultiprocessorScheduling"`: +- Parse `--lengths`, `--num-processors`, `--deadline` flags +- Add required flags to `cli.rs` `CreateArgs` if not already present +- Update help text + +### Step 5: Write unit tests + +Create `src/unit_tests/models/misc/multiprocessor_scheduling.rs`: + +- `test_multiprocessor_scheduling_creation`: construct instance, verify dims = [2, 2, 2, 2, 2] for 5 tasks, 2 processors +- `test_multiprocessor_scheduling_evaluation`: test feasible (true) and infeasible (false) configs +- `test_multiprocessor_scheduling_serialization`: round-trip serde +- `test_multiprocessor_scheduling_solver`: brute-force finds a satisfying assignment for the example + +### Step 6: Document in paper + +Invoke `/write-model-in-paper` to add `#problem-def("MultiprocessorScheduling")` to `docs/paper/reductions.typ`: +- Add `"MultiprocessorScheduling": [Multiprocessor Scheduling]` to `display-name` dict +- Formal definition from Garey & Johnson +- Example with visualization of the 5-task, 2-processor instance +- Reference: Garey & Johnson 1979, Lenstra et al. 1977 + +### Step 7: Verify + +```bash +make test clippy +``` + +Run `/review-implementation` to verify structural and semantic checks. From 6b7efb09e3641c764a248a2e1169c021d9a66690 Mon Sep 17 00:00:00 2001 From: zazabap Date: Tue, 10 Mar 2026 14:44:42 +0000 Subject: [PATCH 2/6] feat: implement MultiprocessorScheduling model for #212 Add a satisfaction problem for multiprocessor scheduling: given tasks with processing times, processors, and a deadline, determine if tasks can be assigned so no processor exceeds the deadline. Co-Authored-By: Claude Opus 4.6 --- problemreductions-cli/src/cli.rs | 13 +- problemreductions-cli/src/commands/create.rs | 31 ++++- problemreductions-cli/src/dispatch.rs | 4 +- problemreductions-cli/src/problem_name.rs | 1 + src/lib.rs | 4 +- src/models/misc/mod.rs | 3 + src/models/misc/multiprocessor_scheduling.rs | 128 +++++++++++++++++ src/models/mod.rs | 2 +- .../models/misc/multiprocessor_scheduling.rs | 131 ++++++++++++++++++ 9 files changed, 312 insertions(+), 5 deletions(-) create mode 100644 src/models/misc/multiprocessor_scheduling.rs create mode 100644 src/unit_tests/models/misc/multiprocessor_scheduling.rs diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 91e9bd252..2c38759bd 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -216,6 +216,7 @@ Flags by problem type: BicliqueCover --left, --right, --biedges, --k BMF --matrix (0/1), --rank CVP --basis, --target-vec [--bounds] + MultiprocessorScheduling --lengths, --num-processors, --deadline ILP, CircuitSAT (via reduction only) Geometry graph variants (use slash notation, e.g., MIS/KingsSubgraph): @@ -231,7 +232,8 @@ Examples: pred create QUBO --matrix \"1,0.5;0.5,2\" pred create MIS/KingsSubgraph --positions \"0,0;1,0;1,1;0,1\" pred create MIS/UnitDiskGraph --positions \"0,0;1,0;0.5,0.8\" --radius 1.5 - pred create MIS --random --num-vertices 10 --edge-prob 0.3")] + pred create MIS --random --num-vertices 10 --edge-prob 0.3 + pred create MultiprocessorScheduling --lengths 4,5,3,2,6 --num-processors 2 --deadline 10")] pub struct CreateArgs { /// Problem type (e.g., MIS, QUBO, SAT) #[arg(value_parser = crate::problem_name::ProblemNameParser)] @@ -326,6 +328,15 @@ pub struct CreateArgs { /// Variable bounds for CVP as "lower,upper" (e.g., "-10,10") [default: -10,10] #[arg(long, allow_hyphen_values = true)] pub bounds: Option, + /// Task processing times for MultiprocessorScheduling (comma-separated, e.g., "4,5,3,2,6") + #[arg(long)] + pub lengths: Option, + /// Number of processors for MultiprocessorScheduling + #[arg(long)] + pub num_processors: Option, + /// Deadline for MultiprocessorScheduling + #[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 418bc520f..b94be862e 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -5,7 +5,7 @@ use crate::problem_name::{parse_problem_spec, resolve_variant}; use crate::util; use anyhow::{bail, Context, Result}; use problemreductions::models::algebraic::{ClosestVectorProblem, BMF}; -use problemreductions::models::misc::{BinPacking, PaintShop}; +use problemreductions::models::misc::{BinPacking, MultiprocessorScheduling, PaintShop}; use problemreductions::prelude::*; use problemreductions::registry::collect_schemas; use problemreductions::topology::{ @@ -45,6 +45,9 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.basis.is_none() && args.target_vec.is_none() && args.bounds.is_none() + && args.lengths.is_none() + && args.num_processors.is_none() + && args.deadline.is_none() } fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { @@ -83,6 +86,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "SpinGlass" => "--graph 0-1,1-2 --couplings 1,1", "KColoring" => "--graph 0-1,1-2,2-0 --k 3", "Factoring" => "--target 15 --m 4 --n 4", + "MultiprocessorScheduling" => "--lengths 4,5,3,2,6 --num-processors 2 --deadline 10", _ => "", } } @@ -443,6 +447,31 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // MultiprocessorScheduling + "MultiprocessorScheduling" => { + let usage = "Usage: pred create MultiprocessorScheduling --lengths 4,5,3,2,6 --num-processors 2 --deadline 10"; + let lengths_str = args.lengths.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "MultiprocessorScheduling requires --lengths, --num-processors, and --deadline\n\n{usage}" + ) + })?; + let num_processors = args.num_processors.ok_or_else(|| { + anyhow::anyhow!("MultiprocessorScheduling requires --num-processors\n\n{usage}") + })?; + let deadline = args.deadline.ok_or_else(|| { + anyhow::anyhow!("MultiprocessorScheduling requires --deadline\n\n{usage}") + })?; + let lengths: Vec = util::parse_comma_list(lengths_str)?; + ( + ser(MultiprocessorScheduling::new( + lengths, + num_processors, + deadline, + ))?, + resolved_variant.clone(), + ) + } + _ => bail!("{}", crate::problem_name::unknown_problem_error(canonical)), }; diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 7a8498421..f341a99ed 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}; +use problemreductions::models::misc::{BinPacking, Knapsack, MultiprocessorScheduling}; use problemreductions::prelude::*; use problemreductions::rules::{MinimizeSteps, ReductionGraph}; use problemreductions::solvers::{BruteForce, ILPSolver, Solver}; @@ -245,6 +245,7 @@ pub fn load_problem( _ => deser_opt::>(data), }, "Knapsack" => deser_opt::(data), + "MultiprocessorScheduling" => deser_sat::(data), _ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)), } } @@ -305,6 +306,7 @@ pub fn serialize_any_problem( _ => try_ser::>(any), }, "Knapsack" => try_ser::(any), + "MultiprocessorScheduling" => 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 acd9b4b59..9d4b77c07 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -52,6 +52,7 @@ pub fn resolve_alias(input: &str) -> String { "binpacking" => "BinPacking".to_string(), "cvp" | "closestvectorproblem" => "ClosestVectorProblem".to_string(), "knapsack" => "Knapsack".to_string(), + "multiprocessorscheduling" => "MultiprocessorScheduling".to_string(), _ => input.to_string(), // pass-through for exact names } } diff --git a/src/lib.rs b/src/lib.rs index c9ada7ef1..66589d413 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,7 +43,9 @@ pub mod prelude { KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumVertexCover, TravelingSalesman, }; - pub use crate::models::misc::{BinPacking, Factoring, Knapsack, PaintShop}; + pub use crate::models::misc::{ + BinPacking, Factoring, Knapsack, MultiprocessorScheduling, PaintShop, + }; pub use crate::models::set::{MaximumSetPacking, MinimumSetCovering}; // Core traits diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index cdb66e969..77712d340 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -4,14 +4,17 @@ //! - [`BinPacking`]: Bin Packing (minimize bins) //! - [`Factoring`]: Integer factorization //! - [`Knapsack`]: 0-1 Knapsack (maximize value subject to weight capacity) +//! - [`MultiprocessorScheduling`]: Schedule tasks on processors to meet a deadline //! - [`PaintShop`]: Minimize color switches in paint shop scheduling mod bin_packing; pub(crate) mod factoring; mod knapsack; +mod multiprocessor_scheduling; pub(crate) mod paintshop; pub use bin_packing::BinPacking; pub use factoring::Factoring; pub use knapsack::Knapsack; +pub use multiprocessor_scheduling::MultiprocessorScheduling; pub use paintshop::PaintShop; diff --git a/src/models/misc/multiprocessor_scheduling.rs b/src/models/misc/multiprocessor_scheduling.rs new file mode 100644 index 000000000..ca7b853fb --- /dev/null +++ b/src/models/misc/multiprocessor_scheduling.rs @@ -0,0 +1,128 @@ +//! Multiprocessor Scheduling problem implementation. +//! +//! The Multiprocessor Scheduling problem asks whether a set of tasks +//! can be assigned to identical processors such that no processor's +//! total load exceeds a given deadline. + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "MultiprocessorScheduling", + module_path: module_path!(), + description: "Assign tasks to processors so that no processor's load exceeds a deadline", + fields: &[ + FieldInfo { name: "lengths", type_name: "Vec", description: "Processing time l(t) for each task" }, + FieldInfo { name: "num_processors", type_name: "u64", description: "Number of identical processors m" }, + FieldInfo { name: "deadline", type_name: "u64", description: "Global deadline D" }, + ], + } +} + +/// The Multiprocessor Scheduling problem. +/// +/// Given a set T of tasks with processing times, a number m of identical +/// processors, and a deadline D, determine whether there exists an assignment +/// of tasks to processors such that the total load on each processor does +/// not exceed D. +/// +/// # Representation +/// +/// Each task has a variable in `{0, ..., m-1}` representing its processor assignment. +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::misc::MultiprocessorScheduling; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// // 5 tasks with lengths [4, 5, 3, 2, 6], 2 processors, deadline 10 +/// let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 10); +/// let solver = BruteForce::new(); +/// let solution = solver.find_satisfying(&problem); +/// assert!(solution.is_some()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MultiprocessorScheduling { + /// Processing time for each task. + lengths: Vec, + /// Number of identical processors. + num_processors: u64, + /// Global deadline. + deadline: u64, +} + +impl MultiprocessorScheduling { + /// Create a new Multiprocessor Scheduling instance. + /// + /// # Panics + /// Panics if `num_processors` is zero. + pub fn new(lengths: Vec, num_processors: u64, deadline: u64) -> Self { + assert!(num_processors > 0, "num_processors must be positive"); + Self { + lengths, + num_processors, + deadline, + } + } + + /// Returns the processing times for each task. + pub fn lengths(&self) -> &[u64] { + &self.lengths + } + + /// Returns the number of processors. + pub fn num_processors(&self) -> u64 { + self.num_processors + } + + /// Returns the deadline. + pub fn deadline(&self) -> u64 { + self.deadline + } + + /// Returns the number of tasks. + pub fn num_tasks(&self) -> usize { + self.lengths.len() + } +} + +impl Problem for MultiprocessorScheduling { + const NAME: &'static str = "MultiprocessorScheduling"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + vec![self.num_processors as usize; self.num_tasks()] + } + + fn evaluate(&self, config: &[usize]) -> bool { + if config.len() != self.num_tasks() { + return false; + } + let m = self.num_processors as usize; + if config.iter().any(|&p| p >= m) { + return false; + } + let mut loads = vec![0u64; m]; + for (i, &processor) in config.iter().enumerate() { + loads[processor] += self.lengths[i]; + } + loads.iter().all(|&load| load <= self.deadline) + } +} + +impl SatisfactionProblem for MultiprocessorScheduling {} + +crate::declare_variants! { + MultiprocessorScheduling => "num_processors ^ num_tasks", +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/multiprocessor_scheduling.rs"] +mod tests; diff --git a/src/models/mod.rs b/src/models/mod.rs index 96b4b79d1..9daa0a0e0 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -15,5 +15,5 @@ pub use graph::{ BicliqueCover, KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumVertexCover, SpinGlass, TravelingSalesman, }; -pub use misc::{BinPacking, Factoring, Knapsack, PaintShop}; +pub use misc::{BinPacking, Factoring, Knapsack, MultiprocessorScheduling, PaintShop}; pub use set::{MaximumSetPacking, MinimumSetCovering}; diff --git a/src/unit_tests/models/misc/multiprocessor_scheduling.rs b/src/unit_tests/models/misc/multiprocessor_scheduling.rs new file mode 100644 index 000000000..504a88c4c --- /dev/null +++ b/src/unit_tests/models/misc/multiprocessor_scheduling.rs @@ -0,0 +1,131 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::traits::Problem; + +#[test] +fn test_multiprocessor_scheduling_basic() { + let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 10); + assert_eq!(problem.num_tasks(), 5); + assert_eq!(problem.lengths(), &[4, 5, 3, 2, 6]); + assert_eq!(problem.num_processors(), 2); + assert_eq!(problem.deadline(), 10); + assert_eq!(problem.dims(), vec![2; 5]); + assert_eq!( + ::NAME, + "MultiprocessorScheduling" + ); + assert_eq!(::variant(), vec![]); +} + +#[test] +fn test_multiprocessor_scheduling_feasible() { + let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 10); + // Processor 0: tasks 0,4 => 4+6=10, Processor 1: tasks 1,2,3 => 5+3+2=10 + assert!(problem.evaluate(&[0, 1, 1, 1, 0])); +} + +#[test] +fn test_multiprocessor_scheduling_infeasible() { + let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 10); + // Processor 0: tasks 0,1,2,3,4 => 4+5+3+2+6=20 > 10 + assert!(!problem.evaluate(&[0, 0, 0, 0, 0])); +} + +#[test] +fn test_multiprocessor_scheduling_infeasible_tight() { + let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 10); + // Processor 0: tasks 0,1,4 => 4+5+6=15 > 10 + assert!(!problem.evaluate(&[0, 0, 1, 1, 0])); +} + +#[test] +fn test_multiprocessor_scheduling_wrong_config_length() { + let problem = MultiprocessorScheduling::new(vec![4, 5, 3], 2, 10); + assert!(!problem.evaluate(&[0, 1])); + assert!(!problem.evaluate(&[0, 1, 0, 1])); +} + +#[test] +fn test_multiprocessor_scheduling_invalid_processor_index() { + let problem = MultiprocessorScheduling::new(vec![4, 5, 3], 2, 10); + // Processor index 2 is out of range for 2 processors + assert!(!problem.evaluate(&[0, 2, 0])); +} + +#[test] +fn test_multiprocessor_scheduling_empty_instance() { + let problem = MultiprocessorScheduling::new(vec![], 2, 10); + assert_eq!(problem.num_tasks(), 0); + assert_eq!(problem.dims(), Vec::::new()); + // Empty assignment is always feasible + assert!(problem.evaluate(&[])); +} + +#[test] +fn test_multiprocessor_scheduling_single_task() { + let problem = MultiprocessorScheduling::new(vec![5], 2, 5); + assert!(problem.evaluate(&[0])); + assert!(problem.evaluate(&[1])); +} + +#[test] +fn test_multiprocessor_scheduling_single_task_exceeds_deadline() { + let problem = MultiprocessorScheduling::new(vec![11], 2, 10); + assert!(!problem.evaluate(&[0])); + assert!(!problem.evaluate(&[1])); +} + +#[test] +fn test_multiprocessor_scheduling_three_processors() { + let problem = MultiprocessorScheduling::new(vec![3, 3, 3], 3, 3); + assert_eq!(problem.dims(), vec![3; 3]); + // One task per processor + assert!(problem.evaluate(&[0, 1, 2])); + // Two tasks on one processor exceeds deadline + assert!(!problem.evaluate(&[0, 0, 1])); +} + +#[test] +fn test_multiprocessor_scheduling_brute_force() { + let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 10); + 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_multiprocessor_scheduling_brute_force_infeasible() { + // Total length = 20, with 2 processors and deadline 9, impossible + let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 9); + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!(solution.is_none()); +} + +#[test] +fn test_multiprocessor_scheduling_serialization() { + let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 10); + let json = serde_json::to_value(&problem).unwrap(); + let restored: MultiprocessorScheduling = serde_json::from_value(json).unwrap(); + assert_eq!(restored.lengths(), problem.lengths()); + assert_eq!(restored.num_processors(), problem.num_processors()); + assert_eq!(restored.deadline(), problem.deadline()); +} + +#[test] +#[should_panic(expected = "num_processors must be positive")] +fn test_multiprocessor_scheduling_zero_processors() { + MultiprocessorScheduling::new(vec![1, 2], 0, 5); +} + +#[test] +fn test_multiprocessor_scheduling_deadline_zero() { + // Only feasible if all lengths are 0 + let problem = MultiprocessorScheduling::new(vec![0, 0], 2, 0); + assert!(problem.evaluate(&[0, 1])); + + let problem2 = MultiprocessorScheduling::new(vec![1, 0], 2, 0); + assert!(!problem2.evaluate(&[0, 1])); +} From aa1e0edc8777c7254451dd89f74f29d90f2328e7 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 12:25:45 +0800 Subject: [PATCH 3/6] chore: remove plan file after implementation --- .../2026-03-10-multiprocessor-scheduling.md | 147 ------------------ 1 file changed, 147 deletions(-) delete mode 100644 docs/plans/2026-03-10-multiprocessor-scheduling.md diff --git a/docs/plans/2026-03-10-multiprocessor-scheduling.md b/docs/plans/2026-03-10-multiprocessor-scheduling.md deleted file mode 100644 index fb22490f7..000000000 --- a/docs/plans/2026-03-10-multiprocessor-scheduling.md +++ /dev/null @@ -1,147 +0,0 @@ -# Plan: [Model] MultiprocessorScheduling (#212) - -## Problem Summary - -**Name:** `MultiprocessorScheduling` -**Reference:** Garey & Johnson, *Computers and Intractability*, A5 SS8 -**Category:** `misc/` (scheduling input: list of processing times + number of machines) - -### Mathematical Definition - -INSTANCE: Set T of tasks, number m of processors, length l(t) for each t in T, and a deadline D. -QUESTION: Is there an assignment of tasks to processors such that the total load on each processor does not exceed D? - -Equivalently: given n jobs with integer processing times and m identical parallel machines, assign each job to a machine such that for every machine i, the sum of processing times of jobs assigned to i is at most D. - -### Problem Type - -**Satisfaction problem** (`Metric = bool`, implements `SatisfactionProblem`). - -The issue defines this as a decision problem: "Is there an m-processor schedule for T that meets the overall deadline D?" A configuration is feasible (true) iff for every processor, the total load does not exceed D. - -### Variables - -- **Count:** n = |T| (one variable per task) -- **Per-variable domain:** {0, 1, ..., m-1} -- the processor index assigned to the task -- **dims():** `vec![num_processors; num_tasks]` - -### Evaluation - -``` -evaluate(config): - for each processor i in 0..m: - load_i = sum of lengths[j] for all j where config[j] == i - if load_i > deadline: return false - return true -``` - -### Struct Fields - -| Field | Type | Description | -|------------------|------------|-------------------------------------| -| `lengths` | `Vec` | Processing time l(t) for each task | -| `num_processors` | `u64` | Number of identical processors m | -| `deadline` | `u64` | Global deadline D | - -### Getter Methods (for overhead system) - -- `num_tasks() -> usize` -- returns `lengths.len()` -- `num_processors() -> u64` -- returns `self.num_processors` - -### Complexity - -- For general m (part of input): strongly NP-hard. No known improvement over O*(m^n) brute-force enumeration. -- For fixed m: weakly NP-hard, solvable by pseudo-polynomial DP in O(n * D^(m-1)). -- Complexity string: `"num_processors ^ num_tasks"` (general case brute force) -- References: Garey & Johnson 1979; Lenstra, Rinnooy Kan & Brucker, *Annals of Discrete Mathematics*, 1977. - -### Solving Strategy - -- BruteForce: enumerate all m^n assignments, check if max load <= D. -- ILP: binary variables x_{t,i}, constraints sum_i x_{t,i} = 1, sum_t x_{t,i} * l(t) <= D. - -### Example Instance - -T = {t1, t2, t3, t4, t5}, lengths = [4, 5, 3, 2, 6], m = 2, D = 10. -Feasible assignment: config = [0, 1, 1, 1, 0] (processor 0 gets {t1, t5} load=10, processor 1 gets {t2, t3, t4} load=10). -Answer: true. - ---- - -## Implementation Steps - -### Step 1: Category - -`misc/` -- scheduling input does not fit graph, formula, set, or algebraic categories. - -### Step 2: Implement the model - -Create `src/models/misc/multiprocessor_scheduling.rs`: - -1. `inventory::submit!` for `ProblemSchemaEntry` with fields: `lengths`, `num_processors`, `deadline` -2. Struct `MultiprocessorScheduling` with `#[derive(Debug, Clone, Serialize, Deserialize)]` -3. Constructor `new(lengths: Vec, num_processors: u64, deadline: u64) -> Self` -4. Accessors: `lengths()`, `num_processors()`, `deadline()`, `num_tasks()` -5. `Problem` impl: - - `NAME = "MultiprocessorScheduling"` - - `Metric = bool` - - `variant() -> crate::variant_params![]` (no type parameters) - - `dims() -> vec![self.num_processors as usize; self.num_tasks()]` - - `evaluate()`: compute load per processor, return true iff all loads <= deadline -6. `SatisfactionProblem` impl (marker trait) -7. `declare_variants! { MultiprocessorScheduling => "num_processors ^ num_tasks" }` -8. `#[cfg(test)] #[path = "..."] mod tests;` - -### Step 2.5: Register variant complexity - -```rust -crate::declare_variants! { - MultiprocessorScheduling => "num_processors ^ num_tasks", -} -``` - -### Step 3: Register the model - -1. `src/models/misc/mod.rs`: add `mod multiprocessor_scheduling;` and `pub use multiprocessor_scheduling::MultiprocessorScheduling;` -2. `src/models/mod.rs`: add to the misc re-export line - -### Step 4: Register in CLI - -1. `problemreductions-cli/src/dispatch.rs`: - - `load_problem()`: add match arm `"MultiprocessorScheduling" => deser_sat::(json)` - - `serialize_any_problem()`: add match arm `"MultiprocessorScheduling" => try_ser::(json)` -2. `problemreductions-cli/src/problem_name.rs`: - - `resolve_alias()`: add `"multiprocessorscheduling" => "MultiprocessorScheduling".to_string()` - - No short alias (no well-established abbreviation in the literature) - -### Step 4.5: Add CLI creation support - -Add match arm in `problemreductions-cli/src/commands/create.rs` for `"MultiprocessorScheduling"`: -- Parse `--lengths`, `--num-processors`, `--deadline` flags -- Add required flags to `cli.rs` `CreateArgs` if not already present -- Update help text - -### Step 5: Write unit tests - -Create `src/unit_tests/models/misc/multiprocessor_scheduling.rs`: - -- `test_multiprocessor_scheduling_creation`: construct instance, verify dims = [2, 2, 2, 2, 2] for 5 tasks, 2 processors -- `test_multiprocessor_scheduling_evaluation`: test feasible (true) and infeasible (false) configs -- `test_multiprocessor_scheduling_serialization`: round-trip serde -- `test_multiprocessor_scheduling_solver`: brute-force finds a satisfying assignment for the example - -### Step 6: Document in paper - -Invoke `/write-model-in-paper` to add `#problem-def("MultiprocessorScheduling")` to `docs/paper/reductions.typ`: -- Add `"MultiprocessorScheduling": [Multiprocessor Scheduling]` to `display-name` dict -- Formal definition from Garey & Johnson -- Example with visualization of the 5-task, 2-processor instance -- Reference: Garey & Johnson 1979, Lenstra et al. 1977 - -### Step 7: Verify - -```bash -make test clippy -``` - -Run `/review-implementation` to verify structural and semantic checks. From 1a73b348434e04a8d64b0516da93ca51ed23033e Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 17 Mar 2026 02:08:33 +0800 Subject: [PATCH 4/6] Remove duplicate MultiprocessorScheduling paper entry --- docs/paper/reductions.typ | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 340418087..87e771ae3 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -2110,25 +2110,6 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS ) ] -#problem-def("MultiprocessorScheduling")[ - Given task lengths $ell_1, ell_2, dots, ell_n in ZZ^+$, a positive integer $m$, and a deadline $D in ZZ^+$, determine whether the tasks can be assigned to $m$ identical processors so that the total assigned load on every processor is at most $D$. -][ - Multiprocessor Scheduling is the classical decision problem SS8 in Garey and Johnson @garey1979. It generalizes Partition: when $m = 2$ and $D$ is half the total work, feasibility asks whether the jobs can be split into two equal-load groups. For fixed $m$, pseudo-polynomial dynamic programming gives $O(n D^(m - 1))$ time algorithms, while the case where $m$ is part of the input is strongly NP-complete; see the scheduling complexity survey of Brucker, Lenstra, and Rinnooy Kan @brucker1977. The brute-force formulation used by this project checks all $m^n$ task-to-processor assignments, and subset-based exact partitioning methods improve the general bound to $O^*(2^n)$ @bjorklund2009. - - Because the processors are identical and there are no precedence constraints, the usual start-time formulation is equivalent to a simpler load-balancing view: once a set of tasks is assigned to a processor, they can be executed back-to-back in any order. A configuration is therefore feasible exactly when every processor's total assigned load is at most $D$. - - *Example.* Consider task lengths $(4, 5, 3, 2, 6)$, $m = 2$, and deadline $D = 10$. Assigning tasks $(t_1, t_5)$ to processor 1 and $(t_2, t_3, t_4)$ to processor 2 yields loads $4 + 6 = 10$ and $5 + 3 + 2 = 10$, so the instance is feasible. - - #table( - columns: (auto, 1fr, auto), - inset: (x: 4pt, y: 3pt), - align: (left, left, center), - table.header([Processor], [Assigned tasks], [Load]), - [1], [$t_1, t_5$], [$10$], - [2], [$t_2, t_3, t_4$], [$10$], - ) -] - #{ let x = load-model-example("MultiprocessorScheduling") let lengths = x.instance.lengths From fc060aa05abbf5d011acbbf66cdf6c9a8136bdfe Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 17 Mar 2026 02:10:22 +0800 Subject: [PATCH 5/6] Show only supported solvers in inspect output --- problemreductions-cli/src/commands/inspect.rs | 15 ++++- problemreductions-cli/src/dispatch.rs | 52 ++++++++++------- problemreductions-cli/tests/cli_tests.rs | 58 +++++++++++++++++++ 3 files changed, 101 insertions(+), 24 deletions(-) diff --git a/problemreductions-cli/src/commands/inspect.rs b/problemreductions-cli/src/commands/inspect.rs index 3a5d37cad..c8fb7c27c 100644 --- a/problemreductions-cli/src/commands/inspect.rs +++ b/problemreductions-cli/src/commands/inspect.rs @@ -40,8 +40,17 @@ fn inspect_problem(pj: &ProblemJson, out: &OutputConfig) -> Result<()> { } text.push_str(&format!("Variables: {}\n", problem.num_variables_dyn())); - // Solvers - text.push_str("Solvers: ilp (default), brute-force\n"); + let solvers = if problem.supports_ilp_solver() { + vec!["ilp", "brute-force"] + } else { + vec!["brute-force"] + }; + let solver_summary = if solvers.first() == Some(&"ilp") { + "ilp (default), brute-force".to_string() + } else { + "brute-force".to_string() + }; + text.push_str(&format!("Solvers: {solver_summary}\n")); // Reductions let outgoing = graph.outgoing_reductions(name); @@ -56,7 +65,7 @@ fn inspect_problem(pj: &ProblemJson, out: &OutputConfig) -> Result<()> { "variant": variant, "size_fields": size_fields, "num_variables": problem.num_variables_dyn(), - "solvers": ["ilp", "brute-force"], + "solvers": solvers, "reduces_to": targets, }); diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 2ed93a3c5..b4ce20aea 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -47,6 +47,11 @@ impl LoadedProblem { Ok(SolveResult { config, evaluation }) } + pub fn supports_ilp_solver(&self) -> bool { + let name = self.problem_name(); + name == "ILP" || self.best_ilp_reduction_path().is_some() + } + /// Solve using the ILP solver. If the problem is not ILP, auto-reduce to ILP first. pub fn solve_with_ilp(&self) -> Result { let name = self.problem_name(); @@ -54,7 +59,26 @@ impl LoadedProblem { return solve_ilp(self.as_any()); } - // Auto-reduce to ILP, solve, and map solution back + let reduction_path = self.best_ilp_reduction_path().ok_or_else(|| { + anyhow::anyhow!( + "No reduction path from {} to ILP. Try `--solver brute-force`, or reduce to a problem that supports ILP.", + name + ) + })?; + let graph = ReductionGraph::new(); + + let chain = graph + .reduce_along_path(&reduction_path, self.as_any()) + .ok_or_else(|| anyhow::anyhow!("Failed to execute reduction chain to ILP"))?; + + let ilp_result = solve_ilp(chain.target_problem_any())?; + let config = chain.extract_solution(&ilp_result.config); + let evaluation = self.evaluate_dyn(&config); + Ok(SolveResult { config, evaluation }) + } + + fn best_ilp_reduction_path(&self) -> Option { + let name = self.problem_name(); let source_variant = self.variant_map(); let graph = ReductionGraph::new(); let ilp_variants = graph.variants_for("ILP"); @@ -62,7 +86,7 @@ impl LoadedProblem { let mut best_path = None; for dv in &ilp_variants { - if let Some(p) = graph.find_cheapest_path( + if let Some(path) = graph.find_cheapest_path( name, &source_variant, "ILP", @@ -70,30 +94,16 @@ impl LoadedProblem { &input_size, &MinimizeSteps, ) { - let is_better = best_path - .as_ref() - .is_none_or(|bp: &problemreductions::rules::ReductionPath| p.len() < bp.len()); + let is_better = best_path.as_ref().is_none_or( + |current: &problemreductions::rules::ReductionPath| path.len() < current.len(), + ); if is_better { - best_path = Some(p); + best_path = Some(path); } } } - let reduction_path = best_path.ok_or_else(|| { - anyhow::anyhow!( - "No reduction path from {} to ILP. Try `--solver brute-force`, or reduce to a problem that supports ILP.", - name - ) - })?; - - let chain = graph - .reduce_along_path(&reduction_path, self.as_any()) - .ok_or_else(|| anyhow::anyhow!("Failed to execute reduction chain to ILP"))?; - - let ilp_result = solve_ilp(chain.target_problem_any())?; - let config = chain.extract_solution(&ilp_result.config); - let evaluation = self.evaluate_dyn(&config); - Ok(SolveResult { config, evaluation }) + best_path } } diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 76ecffc96..f80dbcd15 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -3726,6 +3726,64 @@ fn test_inspect_json_output() { std::fs::remove_file(&result_file).ok(); } +#[test] +fn test_inspect_multiprocessor_scheduling_reports_only_brute_force_solver() { + let problem_file = std::env::temp_dir().join("pred_test_inspect_mps_in.json"); + let result_file = std::env::temp_dir().join("pred_test_inspect_mps_out.json"); + let create_out = pred() + .args([ + "-o", + problem_file.to_str().unwrap(), + "create", + "MultiprocessorScheduling", + "--lengths", + "4,5,3,2,6", + "--num-processors", + "2", + "--deadline", + "10", + ]) + .output() + .unwrap(); + assert!( + create_out.status.success(), + "stderr: {}", + String::from_utf8_lossy(&create_out.stderr) + ); + + let output = pred() + .args([ + "-o", + result_file.to_str().unwrap(), + "inspect", + problem_file.to_str().unwrap(), + ]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let content = std::fs::read_to_string(&result_file).unwrap(); + let json: serde_json::Value = serde_json::from_str(&content).unwrap(); + let solvers: Vec<&str> = json["solvers"] + .as_array() + .expect("solvers should be an array") + .iter() + .map(|v| v.as_str().unwrap()) + .collect(); + assert_eq!( + solvers, + vec!["brute-force"], + "unexpected solvers: {solvers:?}" + ); + + std::fs::remove_file(&problem_file).ok(); + std::fs::remove_file(&result_file).ok(); +} + #[test] fn test_inspect_undirected_two_commodity_integral_flow_reports_size_fields() { let problem_file = std::env::temp_dir().join("pred_test_utcif_inspect_in.json"); From f6b647169bc1a6d6c6d1a0491ed63e65ff41ceee Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Wed, 18 Mar 2026 03:23:11 +0800 Subject: [PATCH 6/6] Fix remaining merge conflict marker in create.rs Co-Authored-By: Claude Opus 4.6 (1M context) --- problemreductions-cli/src/commands/create.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index a1f1cc8ca..2831a9ef5 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -1288,7 +1288,6 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } -<<<<<<< HEAD // MultiprocessorScheduling "MultiprocessorScheduling" => { let usage = "Usage: pred create MultiprocessorScheduling --lengths 4,5,3,2,6 --num-processors 2 --deadline 10";