diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 2b632fad..5e0cf92c 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -78,6 +78,7 @@ "SubsetSum": [Subset Sum], "MinimumFeedbackArcSet": [Minimum Feedback Arc Set], "MinimumFeedbackVertexSet": [Minimum Feedback Vertex Set], + "ResourceConstrainedScheduling": [Resource Constrained Scheduling], "ShortestCommonSupersequence": [Shortest Common Supersequence], "MinimumSumMulticenter": [Minimum Sum Multicenter], "SubgraphIsomorphism": [Subgraph Isomorphism], @@ -1196,6 +1197,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. +] + #problem-def("ShortestCommonSupersequence")[ Given a finite alphabet $Sigma$, a set $R = {r_1, dots, r_m}$ of strings over $Sigma^*$, and a positive integer $K$, determine whether there exists a string $w in Sigma^*$ with $|w| lt.eq K$ such that every string $r_i in R$ is a _subsequence_ of $w$: there exist indices $1 lt.eq j_1 < j_2 < dots < j_(|r_i|) lt.eq |w|$ with $w[j_k] = r_i [k]$ for all $k$. ][ 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/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 0e67c051..5c32ae43 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -239,6 +239,7 @@ Flags by problem type: LCS --strings FAS --arcs [--weights] [--num-vertices] FVS --arcs [--weights] [--num-vertices] + ResourceConstrainedScheduling --num-processors, --resource-bounds, --resource-requirements, --deadline FlowShopScheduling --task-lengths, --deadline [--num-processors] SCS --strings, --bound [--alphabet-size] ILP, CircuitSAT (via reduction only) @@ -382,15 +383,21 @@ 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") + /// Number of processors for ResourceConstrainedScheduling or FlowShopScheduling #[arg(long)] - pub task_lengths: Option, - /// Deadline for FlowShopScheduling + 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 or FlowShopScheduling #[arg(long)] pub deadline: Option, - /// Number of processors/machines for FlowShopScheduling + /// Task lengths for FlowShopScheduling (semicolon-separated rows: "3,4,2;2,3,5;4,1,3") #[arg(long)] - pub num_processors: Option, + pub task_lengths: Option, /// Alphabet size for SCS (optional; inferred from max symbol + 1 if omitted) #[arg(long)] pub alphabet_size: Option, diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index b89d65c9..893e79e0 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -9,7 +9,7 @@ use problemreductions::models::algebraic::{ClosestVectorProblem, BMF}; use problemreductions::models::graph::{GraphPartitioning, HamiltonianPath}; use problemreductions::models::misc::{ BinPacking, FlowShopScheduling, LongestCommonSubsequence, PaintShop, - ShortestCommonSupersequence, SubsetSum, + ResourceConstrainedScheduling, ShortestCommonSupersequence, SubsetSum, }; use problemreductions::prelude::*; use problemreductions::registry::collect_schemas; @@ -57,9 +57,11 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.pattern.is_none() && args.strings.is_none() && args.arcs.is_none() + && args.num_processors.is_none() + && args.resource_bounds.is_none() + && args.resource_requirements.is_none() && args.task_lengths.is_none() && args.deadline.is_none() - && args.num_processors.is_none() && args.alphabet_size.is_none() } @@ -744,6 +746,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(), + ) + } + // OptimalLinearArrangement — graph + bound "OptimalLinearArrangement" => { let (graph, _) = parse_graph(args).map_err(|e| { diff --git a/src/lib.rs b/src/lib.rs index 1f1c99c3..817b7c02 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,7 +56,7 @@ pub mod prelude { }; pub use crate::models::misc::{ BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, PaintShop, - ShortestCommonSupersequence, SubsetSum, + ResourceConstrainedScheduling, ShortestCommonSupersequence, SubsetSum, }; pub use crate::models::set::{MaximumSetPacking, MinimumSetCovering}; diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index 86b71e75..2f534e2a 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -7,6 +7,7 @@ //! - [`Knapsack`]: 0-1 Knapsack (maximize value subject to weight capacity) //! - [`LongestCommonSubsequence`]: Longest Common Subsequence //! - [`PaintShop`]: Minimize color switches in paint shop scheduling +//! - [`ResourceConstrainedScheduling`]: Schedule unit-length tasks on processors with resource constraints //! - [`ShortestCommonSupersequence`]: Find a common supersequence of bounded length //! - [`SubsetSum`]: Find a subset summing to exactly a target value @@ -16,6 +17,7 @@ mod flow_shop_scheduling; mod knapsack; mod longest_common_subsequence; pub(crate) mod paintshop; +pub(crate) mod resource_constrained_scheduling; pub(crate) mod shortest_common_supersequence; mod subset_sum; @@ -25,6 +27,7 @@ pub use flow_shop_scheduling::FlowShopScheduling; pub use knapsack::Knapsack; pub use longest_common_subsequence::LongestCommonSubsequence; pub use paintshop::PaintShop; +pub use resource_constrained_scheduling::ResourceConstrainedScheduling; pub use shortest_common_supersequence::ShortestCommonSupersequence; pub use subset_sum::SubsetSum; @@ -34,5 +37,6 @@ pub(crate) fn canonical_model_example_specs() -> 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 { + 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, + 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 = usage.saturating_add(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! { + default sat ResourceConstrainedScheduling => "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/models/mod.rs b/src/models/mod.rs index e4448805..ce8de27a 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -20,6 +20,6 @@ pub use graph::{ }; pub use misc::{ BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, PaintShop, - ShortestCommonSupersequence, SubsetSum, + ResourceConstrainedScheduling, ShortestCommonSupersequence, 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..d8fc3a34 --- /dev/null +++ b/src/unit_tests/models/misc/resource_constrained_scheduling.rs @@ -0,0 +1,199 @@ +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.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); + 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[..]); +} 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]