From 707af27c53d29f71e0f5f10cfd06e81059ab0f7c Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 08:26:23 +0000 Subject: [PATCH 1/7] Add plan for #502: ResourceConstrainedScheduling model Co-Authored-By: Claude Opus 4.6 --- ...6-03-13-resource-constrained-scheduling.md | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 docs/plans/2026-03-13-resource-constrained-scheduling.md diff --git a/docs/plans/2026-03-13-resource-constrained-scheduling.md b/docs/plans/2026-03-13-resource-constrained-scheduling.md new file mode 100644 index 00000000..bfb7d853 --- /dev/null +++ b/docs/plans/2026-03-13-resource-constrained-scheduling.md @@ -0,0 +1,59 @@ +# Plan: Add ResourceConstrainedScheduling Model + +**Issue:** #502 — [Model] ResourceConstrainedScheduling +**Skill:** add-model +**Date:** 2026-03-13 + +## Information Checklist + +| # | Item | Value | +|---|------|-------| +| 1 | Problem name | `ResourceConstrainedScheduling` | +| 2 | Mathematical definition | Given unit-length tasks T, m processors, r resources with bounds B_i, resource requirements R_i(t), and deadline D, find a schedule σ assigning each task to a time slot in {0,...,D-1} such that at most m tasks run per slot and resource usage per slot does not exceed any bound B_i | +| 3 | Problem type | Satisfaction (decision: does a feasible schedule exist?) | +| 4 | Type parameters | None | +| 5 | Struct fields | `num_tasks: usize`, `num_processors: usize`, `resource_bounds: Vec`, `resource_requirements: Vec>` (n×r), `deadline: u64` | +| 6 | Configuration space | `vec![deadline as usize; num_tasks]` — each task assigned to a time slot | +| 7 | Feasibility check | For each time slot u, count tasks scheduled there ≤ m, and for each resource i, sum of R_i(t) for tasks at u ≤ B_i | +| 8 | Objective function | bool — true if schedule is feasible | +| 9 | Best known exact algorithm | O*(D^n) brute-force enumeration (strongly NP-complete) | +| 10 | Solving strategy | BruteForce (enumerate all D^n assignments) | +| 11 | Category | `misc` (unique input structure: tasks + processors + resources + deadline) | + +## Steps + +### Step 1: Create model file +- Create `src/models/misc/resource_constrained_scheduling.rs` +- Implement struct with `inventory::submit!` for `ProblemSchemaEntry` +- Constructor: `new(num_processors, resource_bounds, resource_requirements, deadline)` + - Derive `num_tasks` from `resource_requirements.len()` +- Accessor methods: `num_tasks()`, `num_processors()`, `resource_bounds()`, `resource_requirements()`, `deadline()`, `num_resources()` +- Implement `Problem` trait with `Metric = bool`, `SatisfactionProblem` +- `dims()` returns `vec![deadline as usize; num_tasks]` +- `evaluate()` checks: config length, values in range, processor capacity per slot, resource bounds per slot +- `variant()` returns `crate::variant_params![]` (no type parameters) +- `declare_variants!` with complexity `"deadline ^ num_tasks"` + +### Step 2: Register the model +- Add to `src/models/misc/mod.rs` +- Add to `src/models/mod.rs` re-exports + +### Step 3: Register in CLI +- Add match arm in `dispatch.rs` `load_problem()` using `deser_sat` +- Add match arm in `dispatch.rs` `serialize_any_problem()` using `try_ser` +- Add lowercase alias in `problem_name.rs` `resolve_alias()` +- Add creation handler in `commands/create.rs` (parse `--num-processors`, `--resource-bounds`, `--resource-requirements`, `--deadline` flags) +- Add CLI flags in `cli.rs` (`CreateArgs` struct) +- Update help text in `cli.rs` `after_help` +- Update `all_data_flags_empty()` in `create.rs` + +### Step 4: Write unit tests +- Create `src/unit_tests/models/misc/resource_constrained_scheduling.rs` +- Tests: creation, evaluation (valid/invalid), solver, serialization, variant, edge cases + +### Step 5: Document in paper +- Add `problem-def("ResourceConstrainedScheduling")` to `docs/paper/reductions.typ` +- Add display name mapping + +### Step 6: Verify +- `make test clippy` From 2332a18d83bb67a45483c1ab0847f51ae27d581f Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 08:33:27 +0000 Subject: [PATCH 2/7] Implement #502: Add ResourceConstrainedScheduling model - Add satisfaction problem model in src/models/misc/ - Implement Problem and SatisfactionProblem traits - Register in CLI (dispatch, alias, create support) - Add comprehensive unit tests (15 tests) - Add problem definition to Typst paper Co-Authored-By: Claude Opus 4.6 --- docs/paper/reductions.typ | 9 + problemreductions-cli/src/cli.rs | 13 ++ problemreductions-cli/src/commands/create.rs | 39 +++- problemreductions-cli/src/dispatch.rs | 4 +- problemreductions-cli/src/problem_name.rs | 1 + src/lib.rs | 4 +- src/models/misc/mod.rs | 3 + .../misc/resource_constrained_scheduling.rs | 192 ++++++++++++++++++ src/models/mod.rs | 2 +- .../misc/resource_constrained_scheduling.rs | 175 ++++++++++++++++ 10 files changed, 438 insertions(+), 4 deletions(-) create mode 100644 src/models/misc/resource_constrained_scheduling.rs create mode 100644 src/unit_tests/models/misc/resource_constrained_scheduling.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 565abdb1..e3ca35b0 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -55,6 +55,7 @@ "ClosestVectorProblem": [Closest Vector Problem], "SubsetSum": [Subset Sum], "MinimumFeedbackVertexSet": [Minimum Feedback Vertex Set], + "ResourceConstrainedScheduling": [Resource Constrained Scheduling], ) // Definition label: "def:" — each definition block must have a matching label @@ -988,6 +989,14 @@ Biclique Cover is equivalent to factoring the biadjacency matrix $M$ of the bipa *Example.* Let $A = {3, 7, 1, 8, 2, 4}$ ($n = 6$) and target $B = 11$. Selecting $A' = {3, 8}$ gives sum $3 + 8 = 11 = B$. Another solution: $A' = {7, 4}$ with sum $7 + 4 = 11 = B$. ] +#problem-def("ResourceConstrainedScheduling")[ + Given a set $T$ of $n$ unit-length tasks, $m$ identical processors, $r$ resources with bounds $B_i$ ($1 <= i <= r$), resource requirements $R_i (t)$ for each task $t$ and resource $i$ ($0 <= R_i (t) <= B_i$), and an overall deadline $D in ZZ^+$, determine whether there exists an $m$-processor schedule $sigma : T -> {0, dots, D-1}$ such that for every time slot $u$, at most $m$ tasks are scheduled at $u$ and $sum_(t : sigma(t) = u) R_i (t) <= B_i$ for each resource $i$. +][ + RESOURCE CONSTRAINED SCHEDULING is problem SS10 in Garey & Johnson's compendium @garey1979. It is NP-complete in the strong sense, even for $r = 1$ resource and $m = 3$ processors, by reduction from 3-PARTITION @garey1975. For $m = 2$ processors with arbitrary $r$, the problem is solvable in polynomial time via bipartite matching. The general case subsumes bin-packing-style constraints across multiple resource dimensions. + + *Example.* Let $n = 6$ tasks, $m = 3$ processors, $r = 1$ resource with $B_1 = 20$, and deadline $D = 2$. Resource requirements: $R_1(t_1) = 6$, $R_1(t_2) = 7$, $R_1(t_3) = 7$, $R_1(t_4) = 6$, $R_1(t_5) = 8$, $R_1(t_6) = 6$. Schedule: slot 0 $arrow.l {t_1, t_2, t_3}$ (3 tasks, resource $= 20$), slot 1 $arrow.l {t_4, t_5, t_6}$ (3 tasks, resource $= 20$). Both constraints satisfied; answer: YES. +] + // 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 91792e20..1ef9b863 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] + ResourceConstrainedScheduling --num-processors, --resource-bounds, --resource-requirements, --deadline ILP, CircuitSAT (via reduction only) Geometry graph variants (use slash notation, e.g., MIS/KingsSubgraph): @@ -332,6 +333,18 @@ pub struct CreateArgs { /// Directed arcs for directed graph problems (e.g., 0>1,1>2,2>0) #[arg(long)] pub arcs: Option, + /// Number of processors for ResourceConstrainedScheduling + #[arg(long)] + pub num_processors: Option, + /// Resource bounds for ResourceConstrainedScheduling (comma-separated, e.g., "20,15") + #[arg(long)] + pub resource_bounds: Option, + /// Resource requirements for ResourceConstrainedScheduling (semicolon-separated rows, each row comma-separated, e.g., "6,3;7,4;5,2") + #[arg(long)] + pub resource_requirements: Option, + /// Deadline for ResourceConstrainedScheduling + #[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 2df4f099..dde56ddf 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, PaintShop, ResourceConstrainedScheduling}; use problemreductions::prelude::*; use problemreductions::registry::collect_schemas; use problemreductions::topology::{ @@ -48,6 +48,10 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.target_vec.is_none() && args.bounds.is_none() && args.arcs.is_none() + && args.num_processors.is_none() + && args.resource_bounds.is_none() + && args.resource_requirements.is_none() + && args.deadline.is_none() } fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { @@ -459,6 +463,39 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // ResourceConstrainedScheduling + "ResourceConstrainedScheduling" => { + let usage = "Usage: pred create ResourceConstrainedScheduling --num-processors 3 --resource-bounds \"20\" --resource-requirements \"6;7;7;6;8;6\" --deadline 2"; + let num_processors = args + .num_processors + .ok_or_else(|| anyhow::anyhow!("ResourceConstrainedScheduling requires --num-processors\n\n{usage}"))?; + let bounds_str = args.resource_bounds.as_deref().ok_or_else(|| { + anyhow::anyhow!("ResourceConstrainedScheduling requires --resource-bounds\n\n{usage}") + })?; + let reqs_str = args.resource_requirements.as_deref().ok_or_else(|| { + anyhow::anyhow!("ResourceConstrainedScheduling requires --resource-requirements\n\n{usage}") + })?; + let deadline = args + .deadline + .ok_or_else(|| anyhow::anyhow!("ResourceConstrainedScheduling requires --deadline\n\n{usage}"))?; + + let resource_bounds: Vec = util::parse_comma_list(bounds_str)?; + let resource_requirements: Vec> = reqs_str + .split(';') + .map(|row| util::parse_comma_list(row.trim())) + .collect::>>()?; + + ( + ser(ResourceConstrainedScheduling::new( + num_processors, + resource_bounds, + resource_requirements, + 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 49dd523c..3e000021 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -1,6 +1,6 @@ use anyhow::{bail, Context, Result}; use problemreductions::models::algebraic::{ClosestVectorProblem, ILP}; -use problemreductions::models::misc::{BinPacking, Knapsack, SubsetSum}; +use problemreductions::models::misc::{BinPacking, Knapsack, ResourceConstrainedScheduling, 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), + "ResourceConstrainedScheduling" => 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), + "ResourceConstrainedScheduling" => try_ser::(any), "SubsetSum" => try_ser::(any), _ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)), } diff --git a/problemreductions-cli/src/problem_name.rs b/problemreductions-cli/src/problem_name.rs index 2b6c8c73..d1d6b0f3 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(), + "resourceconstrainedscheduling" => "ResourceConstrainedScheduling".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 a74c906f..53998db5 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, Knapsack, PaintShop, ResourceConstrainedScheduling, SubsetSum, + }; pub use crate::models::set::{MaximumSetPacking, MinimumSetCovering}; // Core traits diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index 36ebe905..4ef9f54c 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -5,16 +5,19 @@ //! - [`Factoring`]: Integer factorization //! - [`Knapsack`]: 0-1 Knapsack (maximize value subject to weight capacity) //! - [`PaintShop`]: Minimize color switches in paint shop scheduling +//! - [`ResourceConstrainedScheduling`]: Schedule unit-length tasks on processors with resource constraints //! - [`SubsetSum`]: Find a subset summing to exactly a target value mod bin_packing; pub(crate) mod factoring; mod knapsack; pub(crate) mod paintshop; +mod resource_constrained_scheduling; mod subset_sum; pub use bin_packing::BinPacking; pub use factoring::Factoring; pub use knapsack::Knapsack; pub use paintshop::PaintShop; +pub use resource_constrained_scheduling::ResourceConstrainedScheduling; pub use subset_sum::SubsetSum; diff --git a/src/models/misc/resource_constrained_scheduling.rs b/src/models/misc/resource_constrained_scheduling.rs new file mode 100644 index 00000000..fcfa7562 --- /dev/null +++ b/src/models/misc/resource_constrained_scheduling.rs @@ -0,0 +1,192 @@ +//! Resource Constrained Scheduling problem implementation. +//! +//! A classical NP-complete scheduling problem (Garey & Johnson A5 SS10) where +//! unit-length tasks must be assigned to identical processors under both a +//! processor capacity limit and resource usage constraints per time slot. + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "ResourceConstrainedScheduling", + module_path: module_path!(), + description: "Schedule unit-length tasks on m processors with resource constraints and a deadline", + fields: &[ + FieldInfo { name: "num_processors", type_name: "usize", description: "Number of identical processors m" }, + FieldInfo { name: "resource_bounds", type_name: "Vec", description: "Resource bound B_i for each resource i" }, + FieldInfo { name: "resource_requirements", type_name: "Vec>", description: "R_i(t) for each task t and resource i (n x r matrix)" }, + FieldInfo { name: "deadline", type_name: "u64", description: "Overall deadline D" }, + ], + } +} + +/// The Resource Constrained Scheduling problem. +/// +/// Given `n` unit-length tasks, `m` identical processors, `r` resources with +/// bounds `B_i`, resource requirements `R_i(t)` for each task `t` and resource `i`, +/// and an overall deadline `D`, determine whether there exists a schedule +/// `σ: T → {0, ..., D-1}` such that: +/// - At each time slot `u`, at most `m` tasks are scheduled (processor capacity) +/// - At each time slot `u` and for each resource `i`, the sum of `R_i(t)` over +/// all tasks `t` scheduled at `u` does not exceed `B_i` +/// +/// # Representation +/// +/// Each task has a variable in `{0, ..., D-1}` representing its assigned time slot. +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::misc::ResourceConstrainedScheduling; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// // 6 tasks, 3 processors, 1 resource with bound 20, deadline 2 +/// let problem = ResourceConstrainedScheduling::new( +/// 3, +/// vec![20], +/// vec![vec![6], vec![7], vec![7], vec![6], vec![8], vec![6]], +/// 2, +/// ); +/// let solver = BruteForce::new(); +/// let solution = solver.find_satisfying(&problem); +/// assert!(solution.is_some()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ResourceConstrainedScheduling { + /// Number of identical processors. + num_processors: usize, + /// Resource bounds B_i for each resource. + resource_bounds: Vec, + /// Resource requirements R_i(t) for each task t and resource i (n x r matrix). + resource_requirements: Vec>, + /// Overall deadline D. + deadline: u64, +} + +impl ResourceConstrainedScheduling { + /// Create a new Resource Constrained Scheduling instance. + /// + /// # Arguments + /// * `num_processors` - Number of identical processors `m` + /// * `resource_bounds` - Resource bound `B_i` for each resource `i` (length = r) + /// * `resource_requirements` - `R_i(t)` for each task `t` and resource `i` (n x r matrix) + /// * `deadline` - Overall deadline `D` + pub fn new( + num_processors: usize, + resource_bounds: Vec, + resource_requirements: Vec>, + deadline: u64, + ) -> Self { + Self { + num_processors, + resource_bounds, + resource_requirements, + deadline, + } + } + + /// Get the number of tasks. + pub fn num_tasks(&self) -> usize { + self.resource_requirements.len() + } + + /// Get the number of processors. + pub fn num_processors(&self) -> usize { + self.num_processors + } + + /// Get the resource bounds. + pub fn resource_bounds(&self) -> &[u64] { + &self.resource_bounds + } + + /// Get the resource requirements matrix. + pub fn resource_requirements(&self) -> &[Vec] { + &self.resource_requirements + } + + /// Get the deadline. + pub fn deadline(&self) -> u64 { + self.deadline + } + + /// Get the number of resources. + pub fn num_resources(&self) -> usize { + self.resource_bounds.len() + } +} + +impl Problem for ResourceConstrainedScheduling { + const NAME: &'static str = "ResourceConstrainedScheduling"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + vec![self.deadline as usize; self.num_tasks()] + } + + fn evaluate(&self, config: &[usize]) -> bool { + let n = self.num_tasks(); + let d = self.deadline as usize; + let r = self.num_resources(); + + // Check config length + if config.len() != n { + return false; + } + + // Check all time slots are in range + if config.iter().any(|&slot| slot >= d) { + return false; + } + + // Check processor capacity and resource constraints at each time slot + for u in 0..d { + // Collect tasks scheduled at time slot u + let mut task_count = 0usize; + let mut resource_usage = vec![0u64; r]; + + for (t, &slot) in config.iter().enumerate() { + if slot == u { + task_count += 1; + // Accumulate resource usage + for (usage, &req) in resource_usage + .iter_mut() + .zip(self.resource_requirements[t].iter()) + { + *usage += req; + } + } + } + + // Check processor capacity + if task_count > self.num_processors { + return false; + } + + // Check resource bounds + for (usage, bound) in resource_usage.iter().zip(self.resource_bounds.iter()) { + if usage > bound { + return false; + } + } + } + + true + } +} + +impl SatisfactionProblem for ResourceConstrainedScheduling {} + +crate::declare_variants! { + ResourceConstrainedScheduling => "deadline ^ num_tasks", +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/resource_constrained_scheduling.rs"] +mod tests; diff --git a/src/models/mod.rs b/src/models/mod.rs index ceb584ce..8608c019 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -16,5 +16,5 @@ pub use graph::{ MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumFeedbackVertexSet, MinimumVertexCover, SpinGlass, TravelingSalesman, }; -pub use misc::{BinPacking, Factoring, Knapsack, PaintShop, SubsetSum}; +pub use misc::{BinPacking, Factoring, Knapsack, PaintShop, ResourceConstrainedScheduling, SubsetSum}; pub use set::{MaximumSetPacking, MinimumSetCovering}; diff --git a/src/unit_tests/models/misc/resource_constrained_scheduling.rs b/src/unit_tests/models/misc/resource_constrained_scheduling.rs new file mode 100644 index 00000000..a6bfc8c3 --- /dev/null +++ b/src/unit_tests/models/misc/resource_constrained_scheduling.rs @@ -0,0 +1,175 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::traits::Problem; + +#[test] +fn test_resource_constrained_scheduling_creation() { + let problem = ResourceConstrainedScheduling::new( + 3, + vec![20], + vec![vec![6], vec![7], vec![7], vec![6], vec![8], vec![6]], + 2, + ); + assert_eq!(problem.num_tasks(), 6); + assert_eq!(problem.num_processors(), 3); + assert_eq!(problem.resource_bounds(), &[20]); + assert_eq!(problem.deadline(), 2); + assert_eq!(problem.num_resources(), 1); + assert_eq!(problem.dims().len(), 6); + // Each variable has domain {0, 1} (deadline = 2) + assert!(problem.dims().iter().all(|&d| d == 2)); +} + +#[test] +fn test_resource_constrained_scheduling_evaluate_valid() { + // 6 tasks, 3 processors, 1 resource B_1=20, deadline 2 + // Slot 0: {t1, t2, t3} -> 3 tasks <= 3 processors, resource = 6+7+7 = 20 <= 20 + // Slot 1: {t4, t5, t6} -> 3 tasks <= 3 processors, resource = 6+8+6 = 20 <= 20 + let problem = ResourceConstrainedScheduling::new( + 3, + vec![20], + vec![vec![6], vec![7], vec![7], vec![6], vec![8], vec![6]], + 2, + ); + assert!(problem.evaluate(&[0, 0, 0, 1, 1, 1])); +} + +#[test] +fn test_resource_constrained_scheduling_evaluate_invalid_processor_capacity() { + // 4 tasks, 2 processors, deadline 2 + // Slot 0: {t1, t2, t3} -> 3 tasks > 2 processors + let problem = ResourceConstrainedScheduling::new( + 2, + vec![100], + vec![vec![1], vec![1], vec![1], vec![1]], + 2, + ); + assert!(!problem.evaluate(&[0, 0, 0, 1])); +} + +#[test] +fn test_resource_constrained_scheduling_evaluate_invalid_resource() { + // 4 tasks, 4 processors, 1 resource B_1=10, deadline 2 + // Slot 0: {t1, t2} -> resource = 6+6 = 12 > 10 + let problem = ResourceConstrainedScheduling::new( + 4, + vec![10], + vec![vec![6], vec![6], vec![3], vec![3]], + 2, + ); + assert!(!problem.evaluate(&[0, 0, 1, 1])); +} + +#[test] +fn test_resource_constrained_scheduling_evaluate_wrong_config_length() { + let problem = ResourceConstrainedScheduling::new(3, vec![20], vec![vec![5], vec![5], vec![5]], 2); + assert!(!problem.evaluate(&[0, 1])); + assert!(!problem.evaluate(&[0, 1, 0, 1])); +} + +#[test] +fn test_resource_constrained_scheduling_evaluate_out_of_range_slot() { + let problem = ResourceConstrainedScheduling::new(3, vec![20], vec![vec![5], vec![5], vec![5]], 2); + // Slot 2 is out of range for deadline=2 (valid: 0, 1) + assert!(!problem.evaluate(&[0, 1, 2])); +} + +#[test] +fn test_resource_constrained_scheduling_multiple_resources() { + // 3 tasks, 2 processors, 2 resources with bounds [10, 8], deadline 2 + // Task requirements: t1=[5,4], t2=[5,4], t3=[5,4] + // Slot 0: {t1, t2} -> processor ok, res1=10<=10, res2=8<=8 + // Slot 1: {t3} -> ok + let problem = ResourceConstrainedScheduling::new( + 2, + vec![10, 8], + vec![vec![5, 4], vec![5, 4], vec![5, 4]], + 2, + ); + assert!(problem.evaluate(&[0, 0, 1])); + // Slot 0: {t1, t2, t3} -> 3 > 2 processors + assert!(!problem.evaluate(&[0, 0, 0])); +} + +#[test] +fn test_resource_constrained_scheduling_empty_tasks() { + let problem = ResourceConstrainedScheduling::new(2, vec![10], Vec::>::new(), 3); + assert_eq!(problem.num_tasks(), 0); + assert_eq!(problem.dims(), Vec::::new()); + assert!(problem.evaluate(&[])); +} + +#[test] +fn test_resource_constrained_scheduling_brute_force_solver() { + // 4 tasks, 2 processors, 1 resource B_1=10, deadline 2 + // Tasks: [5, 5, 5, 5] -- each slot can hold 2 tasks (resource: 10=10) + let problem = ResourceConstrainedScheduling::new( + 2, + vec![10], + vec![vec![5], vec![5], vec![5], vec![5]], + 2, + ); + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!(solution.is_some()); + let sol = solution.unwrap(); + assert!(problem.evaluate(&sol)); +} + +#[test] +fn test_resource_constrained_scheduling_brute_force_infeasible() { + // 4 tasks, 1 processor, deadline 2 -> can only do 2 tasks total, but we have 4 + let problem = ResourceConstrainedScheduling::new( + 1, + vec![100], + vec![vec![1], vec![1], vec![1], vec![1]], + 2, + ); + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + // 1 processor * 2 time slots = 2 tasks max, but we have 4 + assert!(solution.is_none()); +} + +#[test] +fn test_resource_constrained_scheduling_problem_name() { + assert_eq!( + ::NAME, + "ResourceConstrainedScheduling" + ); +} + +#[test] +fn test_resource_constrained_scheduling_variant() { + let v = ::variant(); + assert!(v.is_empty()); +} + +#[test] +fn test_resource_constrained_scheduling_serialization() { + let problem = ResourceConstrainedScheduling::new( + 3, + vec![20], + vec![vec![6], vec![7], vec![7], vec![6], vec![8], vec![6]], + 2, + ); + let json = serde_json::to_value(&problem).unwrap(); + let restored: ResourceConstrainedScheduling = serde_json::from_value(json).unwrap(); + assert_eq!(restored.num_tasks(), problem.num_tasks()); + assert_eq!(restored.num_processors(), problem.num_processors()); + assert_eq!(restored.resource_bounds(), problem.resource_bounds()); + assert_eq!(restored.deadline(), problem.deadline()); +} + +#[test] +fn test_resource_constrained_scheduling_single_task() { + let problem = ResourceConstrainedScheduling::new(1, vec![5], vec![vec![5]], 1); + assert!(problem.evaluate(&[0])); +} + +#[test] +fn test_resource_constrained_scheduling_resource_requirements_accessor() { + let reqs = vec![vec![5, 3], vec![2, 4]]; + let problem = ResourceConstrainedScheduling::new(2, vec![10, 10], reqs.clone(), 2); + assert_eq!(problem.resource_requirements(), &reqs[..]); +} From 8e50c9fe96f0065f49166034385fa4804e74326a Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 08:33:32 +0000 Subject: [PATCH 3/7] chore: remove plan file after implementation Co-Authored-By: Claude Opus 4.6 --- ...6-03-13-resource-constrained-scheduling.md | 59 ------------------- 1 file changed, 59 deletions(-) delete mode 100644 docs/plans/2026-03-13-resource-constrained-scheduling.md diff --git a/docs/plans/2026-03-13-resource-constrained-scheduling.md b/docs/plans/2026-03-13-resource-constrained-scheduling.md deleted file mode 100644 index bfb7d853..00000000 --- a/docs/plans/2026-03-13-resource-constrained-scheduling.md +++ /dev/null @@ -1,59 +0,0 @@ -# Plan: Add ResourceConstrainedScheduling Model - -**Issue:** #502 — [Model] ResourceConstrainedScheduling -**Skill:** add-model -**Date:** 2026-03-13 - -## Information Checklist - -| # | Item | Value | -|---|------|-------| -| 1 | Problem name | `ResourceConstrainedScheduling` | -| 2 | Mathematical definition | Given unit-length tasks T, m processors, r resources with bounds B_i, resource requirements R_i(t), and deadline D, find a schedule σ assigning each task to a time slot in {0,...,D-1} such that at most m tasks run per slot and resource usage per slot does not exceed any bound B_i | -| 3 | Problem type | Satisfaction (decision: does a feasible schedule exist?) | -| 4 | Type parameters | None | -| 5 | Struct fields | `num_tasks: usize`, `num_processors: usize`, `resource_bounds: Vec`, `resource_requirements: Vec>` (n×r), `deadline: u64` | -| 6 | Configuration space | `vec![deadline as usize; num_tasks]` — each task assigned to a time slot | -| 7 | Feasibility check | For each time slot u, count tasks scheduled there ≤ m, and for each resource i, sum of R_i(t) for tasks at u ≤ B_i | -| 8 | Objective function | bool — true if schedule is feasible | -| 9 | Best known exact algorithm | O*(D^n) brute-force enumeration (strongly NP-complete) | -| 10 | Solving strategy | BruteForce (enumerate all D^n assignments) | -| 11 | Category | `misc` (unique input structure: tasks + processors + resources + deadline) | - -## Steps - -### Step 1: Create model file -- Create `src/models/misc/resource_constrained_scheduling.rs` -- Implement struct with `inventory::submit!` for `ProblemSchemaEntry` -- Constructor: `new(num_processors, resource_bounds, resource_requirements, deadline)` - - Derive `num_tasks` from `resource_requirements.len()` -- Accessor methods: `num_tasks()`, `num_processors()`, `resource_bounds()`, `resource_requirements()`, `deadline()`, `num_resources()` -- Implement `Problem` trait with `Metric = bool`, `SatisfactionProblem` -- `dims()` returns `vec![deadline as usize; num_tasks]` -- `evaluate()` checks: config length, values in range, processor capacity per slot, resource bounds per slot -- `variant()` returns `crate::variant_params![]` (no type parameters) -- `declare_variants!` with complexity `"deadline ^ num_tasks"` - -### Step 2: Register the model -- Add to `src/models/misc/mod.rs` -- Add to `src/models/mod.rs` re-exports - -### Step 3: Register in CLI -- Add match arm in `dispatch.rs` `load_problem()` using `deser_sat` -- Add match arm in `dispatch.rs` `serialize_any_problem()` using `try_ser` -- Add lowercase alias in `problem_name.rs` `resolve_alias()` -- Add creation handler in `commands/create.rs` (parse `--num-processors`, `--resource-bounds`, `--resource-requirements`, `--deadline` flags) -- Add CLI flags in `cli.rs` (`CreateArgs` struct) -- Update help text in `cli.rs` `after_help` -- Update `all_data_flags_empty()` in `create.rs` - -### Step 4: Write unit tests -- Create `src/unit_tests/models/misc/resource_constrained_scheduling.rs` -- Tests: creation, evaluation (valid/invalid), solver, serialization, variant, edge cases - -### Step 5: Document in paper -- Add `problem-def("ResourceConstrainedScheduling")` to `docs/paper/reductions.typ` -- Add display name mapping - -### Step 6: Verify -- `make test clippy` From 846e5e5d02a49e2a2067ae922c526f9babd67626 Mon Sep 17 00:00:00 2001 From: zazabap Date: Sun, 15 Mar 2026 08:35:54 +0000 Subject: [PATCH 4/7] Merge origin/main and fix ResourceConstrainedScheduling for updated APIs Update ProblemSchemaEntry to include display_name, aliases, dimensions fields. Update declare_variants! to use 'default sat' prefix. Co-Authored-By: Claude Opus 4.6 --- docs/src/reductions/problem_schemas.json | 26 +++++++++++++ docs/src/reductions/reduction_graph.json | 39 +++++++++++-------- .../misc/resource_constrained_scheduling.rs | 5 ++- 3 files changed, 53 insertions(+), 17 deletions(-) diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index 43b2a445..55fdb32e 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -515,6 +515,32 @@ } ] }, + { + "name": "ResourceConstrainedScheduling", + "description": "Schedule unit-length tasks on m processors with resource constraints and a deadline", + "fields": [ + { + "name": "num_processors", + "type_name": "usize", + "description": "Number of identical processors m" + }, + { + "name": "resource_bounds", + "type_name": "Vec", + "description": "Resource bound B_i for each resource i" + }, + { + "name": "resource_requirements", + "type_name": "Vec>", + "description": "R_i(t) for each task t and resource i (n x r matrix)" + }, + { + "name": "deadline", + "type_name": "u64", + "description": "Overall deadline D" + } + ] + }, { "name": "RuralPostman", "description": "Find a circuit covering required edges with total length at most B (Rural Postman Problem)", diff --git a/docs/src/reductions/reduction_graph.json b/docs/src/reductions/reduction_graph.json index bb8c0255..065b977b 100644 --- a/docs/src/reductions/reduction_graph.json +++ b/docs/src/reductions/reduction_graph.json @@ -433,6 +433,13 @@ "doc_path": "models/algebraic/struct.QUBO.html", "complexity": "2^num_vars" }, + { + "name": "ResourceConstrainedScheduling", + "variant": {}, + "category": "misc", + "doc_path": "models/misc/struct.ResourceConstrainedScheduling.html", + "complexity": "deadline ^ num_tasks" + }, { "name": "RuralPostman", "variant": { @@ -535,7 +542,7 @@ }, { "source": 4, - "target": 52, + "target": 53, "overhead": [ { "field": "num_spins", @@ -699,7 +706,7 @@ }, { "source": 20, - "target": 54, + "target": 55, "overhead": [ { "field": "num_elements", @@ -710,7 +717,7 @@ }, { "source": 21, - "target": 49, + "target": 50, "overhead": [ { "field": "num_clauses", @@ -755,7 +762,7 @@ }, { "source": 24, - "target": 52, + "target": 53, "overhead": [ { "field": "num_spins", @@ -1201,7 +1208,7 @@ }, { "source": 47, - "target": 51, + "target": 52, "overhead": [ { "field": "num_spins", @@ -1211,7 +1218,7 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 49, + "source": 50, "target": 4, "overhead": [ { @@ -1226,7 +1233,7 @@ "doc_path": "rules/sat_circuitsat/index.html" }, { - "source": 49, + "source": 50, "target": 15, "overhead": [ { @@ -1241,7 +1248,7 @@ "doc_path": "rules/sat_coloring/index.html" }, { - "source": 49, + "source": 50, "target": 20, "overhead": [ { @@ -1256,7 +1263,7 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 49, + "source": 50, "target": 29, "overhead": [ { @@ -1271,7 +1278,7 @@ "doc_path": "rules/sat_maximumindependentset/index.html" }, { - "source": 49, + "source": 50, "target": 38, "overhead": [ { @@ -1286,7 +1293,7 @@ "doc_path": "rules/sat_minimumdominatingset/index.html" }, { - "source": 51, + "source": 52, "target": 47, "overhead": [ { @@ -1297,7 +1304,7 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 52, + "source": 53, "target": 24, "overhead": [ { @@ -1312,8 +1319,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 52, - "target": 51, + "source": 53, + "target": 52, "overhead": [ { "field": "num_spins", @@ -1327,7 +1334,7 @@ "doc_path": "rules/spinglass_casts/index.html" }, { - "source": 55, + "source": 56, "target": 11, "overhead": [ { @@ -1342,7 +1349,7 @@ "doc_path": "rules/travelingsalesman_ilp/index.html" }, { - "source": 55, + "source": 56, "target": 47, "overhead": [ { diff --git a/src/models/misc/resource_constrained_scheduling.rs b/src/models/misc/resource_constrained_scheduling.rs index fcfa7562..1038a288 100644 --- a/src/models/misc/resource_constrained_scheduling.rs +++ b/src/models/misc/resource_constrained_scheduling.rs @@ -11,6 +11,9 @@ use serde::{Deserialize, Serialize}; inventory::submit! { ProblemSchemaEntry { name: "ResourceConstrainedScheduling", + display_name: "Resource Constrained Scheduling", + aliases: &[], + dimensions: &[], module_path: module_path!(), description: "Schedule unit-length tasks on m processors with resource constraints and a deadline", fields: &[ @@ -184,7 +187,7 @@ impl Problem for ResourceConstrainedScheduling { impl SatisfactionProblem for ResourceConstrainedScheduling {} crate::declare_variants! { - ResourceConstrainedScheduling => "deadline ^ num_tasks", + default sat ResourceConstrainedScheduling => "deadline ^ num_tasks", } #[cfg(test)] From ae53ab89043233aa5d3f27e005e9bfb710d6b6b3 Mon Sep 17 00:00:00 2001 From: zazabap Date: Sun, 15 Mar 2026 08:37:09 +0000 Subject: [PATCH 5/7] Address Copilot review: validate inputs and use saturating_add - Validate resource_requirements shape matches resource_bounds length in new() - Assert deadline > 0 - Use saturating_add for resource usage accumulation to prevent overflow Co-Authored-By: Claude Opus 4.6 --- src/models/misc/resource_constrained_scheduling.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/models/misc/resource_constrained_scheduling.rs b/src/models/misc/resource_constrained_scheduling.rs index 1038a288..085ed016 100644 --- a/src/models/misc/resource_constrained_scheduling.rs +++ b/src/models/misc/resource_constrained_scheduling.rs @@ -82,6 +82,16 @@ impl ResourceConstrainedScheduling { resource_requirements: Vec>, deadline: u64, ) -> Self { + assert!(deadline > 0, "deadline must be positive"); + let r = resource_bounds.len(); + for (t, row) in resource_requirements.iter().enumerate() { + assert_eq!( + row.len(), + r, + "task {t} has {} resource requirements, expected {r}", + row.len() + ); + } Self { num_processors, resource_bounds, @@ -162,7 +172,7 @@ impl Problem for ResourceConstrainedScheduling { .iter_mut() .zip(self.resource_requirements[t].iter()) { - *usage += req; + *usage = usage.saturating_add(req); } } } From 1ab9fce9f352ddc8baf1af69e81e404828c759ee Mon Sep 17 00:00:00 2001 From: zazabap Date: Sun, 15 Mar 2026 08:40:23 +0000 Subject: [PATCH 6/7] Add canonical example-db entry and trait_consistency check - Add canonical_model_example_specs for ResourceConstrainedScheduling - Register in misc/mod.rs example specs chain - Add trait_consistency test entry Co-Authored-By: Claude Opus 4.6 --- src/models/misc/mod.rs | 3 ++- .../misc/resource_constrained_scheduling.rs | 20 +++++++++++++++++++ src/unit_tests/trait_consistency.rs | 4 ++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index 9606c35e..2f534e2a 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -17,7 +17,7 @@ mod flow_shop_scheduling; mod knapsack; mod longest_common_subsequence; pub(crate) mod paintshop; -mod resource_constrained_scheduling; +pub(crate) mod resource_constrained_scheduling; pub(crate) mod shortest_common_supersequence; mod subset_sum; @@ -37,5 +37,6 @@ pub(crate) fn canonical_model_example_specs() -> Vec "deadline ^ num_tasks", } +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "resource_constrained_scheduling", + build: || { + // 6 tasks, 3 processors, 1 resource B_1=20, deadline 2 + let problem = ResourceConstrainedScheduling::new( + 3, + vec![20], + vec![vec![6], vec![7], vec![7], vec![6], vec![8], vec![6]], + 2, + ); + crate::example_db::specs::satisfaction_example( + problem, + vec![vec![0, 0, 0, 1, 1, 1]], + ) + }, + }] +} + #[cfg(test)] #[path = "../../unit_tests/models/misc/resource_constrained_scheduling.rs"] mod tests; diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index ebbc68a0..e65bc614 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -122,6 +122,10 @@ fn test_all_problems_implement_trait_correctly() { &FlowShopScheduling::new(2, vec![vec![1, 2], vec![3, 4]], 10), "FlowShopScheduling", ); + check_problem_trait( + &ResourceConstrainedScheduling::new(3, vec![20], vec![vec![6], vec![7], vec![7]], 2), + "ResourceConstrainedScheduling", + ); } #[test] From c2e1651789fa8fe44dcdea0f236ab8a1df048aeb Mon Sep 17 00:00:00 2001 From: zazabap Date: Sun, 15 Mar 2026 08:41:11 +0000 Subject: [PATCH 7/7] Improve test coverage: serialization, panics, adversarial cases - Check resource_requirements round-trip in serialization test - Add #[should_panic] tests for zero deadline and mismatched requirements - Add test for single task exceeding resource bound (infeasible instance) Co-Authored-By: Claude Opus 4.6 --- .../misc/resource_constrained_scheduling.rs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/unit_tests/models/misc/resource_constrained_scheduling.rs b/src/unit_tests/models/misc/resource_constrained_scheduling.rs index a6bfc8c3..d8fc3a34 100644 --- a/src/unit_tests/models/misc/resource_constrained_scheduling.rs +++ b/src/unit_tests/models/misc/resource_constrained_scheduling.rs @@ -158,9 +158,33 @@ fn test_resource_constrained_scheduling_serialization() { assert_eq!(restored.num_tasks(), problem.num_tasks()); assert_eq!(restored.num_processors(), problem.num_processors()); assert_eq!(restored.resource_bounds(), problem.resource_bounds()); + assert_eq!(restored.resource_requirements(), problem.resource_requirements()); assert_eq!(restored.deadline(), problem.deadline()); } +#[test] +#[should_panic(expected = "deadline must be positive")] +fn test_resource_constrained_scheduling_zero_deadline() { + ResourceConstrainedScheduling::new(2, vec![10], vec![vec![5]], 0); +} + +#[test] +#[should_panic(expected = "resource requirements")] +fn test_resource_constrained_scheduling_mismatched_requirements() { + // 2 resource bounds but task has only 1 requirement + ResourceConstrainedScheduling::new(2, vec![10, 20], vec![vec![5]], 2); +} + +#[test] +fn test_resource_constrained_scheduling_single_task_exceeds_bound() { + // One task requires resource 15 but bound is 10 — instance is infeasible + let problem = ResourceConstrainedScheduling::new(2, vec![10], vec![vec![15]], 2); + assert!(!problem.evaluate(&[0])); + assert!(!problem.evaluate(&[1])); + let solver = BruteForce::new(); + assert!(solver.find_satisfying(&problem).is_none()); +} + #[test] fn test_resource_constrained_scheduling_single_task() { let problem = ResourceConstrainedScheduling::new(1, vec![5], vec![vec![5]], 1);