From 7ac9ce5c3445c702a725a8e2d86e89e2efa8de7b Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 09:56:03 +0000 Subject: [PATCH 1/4] Fix #494: Add SequencingWithReleaseTimesAndDeadlines model Add the single-machine scheduling feasibility problem (Garey & Johnson A5 SS1): given tasks with processing times, release times, and deadlines, determine if a non-preemptive schedule exists. Strongly NP-complete. - Model implementation with Problem/SatisfactionProblem traits - CLI dispatch (load/serialize) and creation support - 13 unit tests covering feasibility, infeasibility, brute-force solver - Paper documentation in reductions.typ - Schema registration Co-Authored-By: Claude Opus 4.6 --- docs/paper/reductions.typ | 9 + docs/src/reductions/problem_schemas.json | 32 ++++ examples/detect_unreachable_from_3sat.rs | 13 +- examples/reduction_binpacking_to_ilp.rs | 14 +- ...duction_longestcommonsubsequence_to_ilp.rs | 10 +- problemreductions-cli/src/cli.rs | 9 + problemreductions-cli/src/commands/create.rs | 48 +++++- problemreductions-cli/src/dispatch.rs | 11 +- problemreductions-cli/src/problem_name.rs | 3 + src/lib.rs | 5 +- src/models/misc/mod.rs | 3 + ...encing_with_release_times_and_deadlines.rs | 163 ++++++++++++++++++ src/models/mod.rs | 5 +- src/rules/longestcommonsubsequence_ilp.rs | 18 +- .../models/misc/longest_common_subsequence.rs | 60 ++----- ...encing_with_release_times_and_deadlines.rs | 154 +++++++++++++++++ .../rules/longestcommonsubsequence_ilp.rs | 48 ++---- 17 files changed, 490 insertions(+), 115 deletions(-) create mode 100644 src/models/misc/sequencing_with_release_times_and_deadlines.rs create mode 100644 src/unit_tests/models/misc/sequencing_with_release_times_and_deadlines.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 16e2297b..faa8c79f 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -56,6 +56,7 @@ "LongestCommonSubsequence": [Longest Common Subsequence], "SubsetSum": [Subset Sum], "MinimumFeedbackVertexSet": [Minimum Feedback Vertex Set], + "SequencingWithReleaseTimesAndDeadlines": [Sequencing with Release Times and Deadlines], ) // Definition label: "def:" — each definition block must have a matching label @@ -997,6 +998,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("SequencingWithReleaseTimesAndDeadlines")[ + Given a set $T$ of $n$ tasks and, for each task $t in T$, a processing time $ell(t) in ZZ^+$, a release time $r(t) in ZZ^(>=0)$, and a deadline $d(t) in ZZ^+$, determine whether there exists a one-processor schedule $sigma: T -> ZZ^(>=0)$ such that for all $t in T$: $sigma(t) >= r(t)$, $sigma(t) + ell(t) <= d(t)$, and no two tasks overlap (i.e., $sigma(t) > sigma(t')$ implies $sigma(t) >= sigma(t') + ell(t')$). +][ + Problem SS1 in Garey and Johnson's appendix @garey1979, and a fundamental single-machine scheduling feasibility problem. It is strongly NP-complete by reduction from 3-Partition, so no pseudo-polynomial time algorithm exists unless P = NP. The problem becomes polynomial-time solvable when: (1) all task lengths equal 1, (2) preemption is allowed, or (3) all release times are zero. The best known exact algorithm for the general case runs in $O^*(2^n dot n)$ time via dynamic programming on task subsets. + + *Example.* Let $T = {t_1, t_2, t_3}$ with $ell(t_1) = 2$, $r(t_1) = 0$, $d(t_1) = 5$; $ell(t_2) = 3$, $r(t_2) = 1$, $d(t_2) = 6$; $ell(t_3) = 1$, $r(t_3) = 2$, $d(t_3) = 4$. A feasible schedule: $sigma(t_1) = 0$ (runs $[0, 2)$), $sigma(t_3) = 2$ (runs $[2, 3)$), $sigma(t_2) = 3$ (runs $[3, 6)$). All release and deadline constraints are satisfied with no overlap. +] + // Completeness check: warn about problem types in JSON but missing from paper #{ let json-models = { diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index ef694f9d..f08abf4f 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -195,6 +195,17 @@ } ] }, + { + "name": "LongestCommonSubsequence", + "description": "Find the longest string that is a subsequence of every input string", + "fields": [ + { + "name": "strings", + "type_name": "Vec>", + "description": "The input strings" + } + ] + }, { "name": "MaxCut", "description": "Find maximum weight cut in a graph", @@ -403,6 +414,27 @@ } ] }, + { + "name": "SequencingWithReleaseTimesAndDeadlines", + "description": "Single-machine scheduling feasibility: can all tasks be scheduled within their release-deadline windows without overlap?", + "fields": [ + { + "name": "lengths", + "type_name": "Vec", + "description": "Processing time l(t) for each task (positive)" + }, + { + "name": "release_times", + "type_name": "Vec", + "description": "Release time r(t) for each task (non-negative)" + }, + { + "name": "deadlines", + "type_name": "Vec", + "description": "Deadline d(t) for each task (positive)" + } + ] + }, { "name": "SpinGlass", "description": "Minimize Ising Hamiltonian on a graph", diff --git a/examples/detect_unreachable_from_3sat.rs b/examples/detect_unreachable_from_3sat.rs index c4867227..b770ae6e 100644 --- a/examples/detect_unreachable_from_3sat.rs +++ b/examples/detect_unreachable_from_3sat.rs @@ -109,8 +109,7 @@ fn main() { // Check if ALL variants of this problem are P-time // (conservative: if any variant could be hard, don't classify as P) let variants = graph.variants_for(name); - variants.len() == 1 - && variants[0].get(*key).map(|s| s.as_str()) == Some(*val) + variants.len() == 1 && variants[0].get(*key).map(|s| s.as_str()) == Some(*val) } } }); @@ -143,10 +142,7 @@ fn main() { } if !p_time.is_empty() { - println!( - "In P — correctly unreachable ({}):", - p_time.len() - ); + println!("In P — correctly unreachable ({}):", p_time.len()); for name in &p_time { println!(" {name}"); } @@ -165,10 +161,7 @@ fn main() { } if !orphans.is_empty() { - println!( - "Orphans — no reductions at all ({}):", - orphans.len() - ); + println!("Orphans — no reductions at all ({}):", orphans.len()); for name in &orphans { println!(" {name}"); } diff --git a/examples/reduction_binpacking_to_ilp.rs b/examples/reduction_binpacking_to_ilp.rs index 104d707a..74b87e1b 100644 --- a/examples/reduction_binpacking_to_ilp.rs +++ b/examples/reduction_binpacking_to_ilp.rs @@ -53,7 +53,10 @@ pub fn run() { // 5. Extract source solution let bp_solution = reduction.extract_solution(&ilp_solution); - println!("Source BinPacking solution (bin assignments): {:?}", bp_solution); + println!( + "Source BinPacking solution (bin assignments): {:?}", + bp_solution + ); // 6. Verify let size = bp.evaluate(&bp_solution); @@ -76,13 +79,8 @@ pub fn run() { let source_variant = variant_to_map(BinPacking::::variant()); let target_variant = variant_to_map(ILP::::variant()); - let overhead = lookup_overhead( - "BinPacking", - &source_variant, - "ILP", - &target_variant, - ) - .unwrap_or_default(); + let overhead = + lookup_overhead("BinPacking", &source_variant, "ILP", &target_variant).unwrap_or_default(); let data = ReductionData { source: ProblemSide { diff --git a/examples/reduction_longestcommonsubsequence_to_ilp.rs b/examples/reduction_longestcommonsubsequence_to_ilp.rs index 5ff1529a..edaad1d5 100644 --- a/examples/reduction_longestcommonsubsequence_to_ilp.rs +++ b/examples/reduction_longestcommonsubsequence_to_ilp.rs @@ -73,9 +73,13 @@ pub fn run() { let source_variant = variant_to_map(LongestCommonSubsequence::variant()); let target_variant = variant_to_map(ILP::::variant()); - let overhead = - lookup_overhead("LongestCommonSubsequence", &source_variant, "ILP", &target_variant) - .expect("LCS -> ILP overhead not found"); + let overhead = lookup_overhead( + "LongestCommonSubsequence", + &source_variant, + "ILP", + &target_variant, + ) + .expect("LCS -> ILP overhead not found"); let data = ReductionData { source: ProblemSide { diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 2eee89ac..71e2d4e8 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -336,6 +336,15 @@ pub struct CreateArgs { /// Directed arcs for directed graph problems (e.g., 0>1,1>2,2>0) #[arg(long)] pub arcs: Option, + /// Task processing times for scheduling problems (comma-separated, e.g., "3,2,4") + #[arg(long)] + pub lengths: Option, + /// Task release times for scheduling problems (comma-separated, e.g., "0,1,5") + #[arg(long)] + pub release_times: Option, + /// Task deadlines for scheduling problems (comma-separated, e.g., "5,6,10") + #[arg(long)] + pub deadlines: Option, } #[derive(clap::Args)] diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index fae9adcd..b57d44ad 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -6,7 +6,9 @@ use crate::util; use anyhow::{bail, Context, Result}; use problemreductions::models::algebraic::{ClosestVectorProblem, BMF}; use problemreductions::models::graph::GraphPartitioning; -use problemreductions::models::misc::{BinPacking, LongestCommonSubsequence, PaintShop}; +use problemreductions::models::misc::{ + BinPacking, LongestCommonSubsequence, PaintShop, SequencingWithReleaseTimesAndDeadlines, +}; use problemreductions::prelude::*; use problemreductions::registry::collect_schemas; use problemreductions::topology::{ @@ -49,6 +51,9 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.bounds.is_none() && args.strings.is_none() && args.arcs.is_none() + && args.lengths.is_none() + && args.release_times.is_none() + && args.deadlines.is_none() } fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { @@ -528,6 +533,47 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // SequencingWithReleaseTimesAndDeadlines + "SequencingWithReleaseTimesAndDeadlines" => { + let lengths_str = args.lengths.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "SequencingWithReleaseTimesAndDeadlines requires --lengths, --release-times, and --deadlines\n\n\ + Usage: pred create SequencingWithReleaseTimesAndDeadlines --lengths 3,2,4 --release-times 0,1,5 --deadlines 5,6,10" + ) + })?; + let release_str = args.release_times.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "SequencingWithReleaseTimesAndDeadlines requires --release-times\n\n\ + Usage: pred create SequencingWithReleaseTimesAndDeadlines --lengths 3,2,4 --release-times 0,1,5 --deadlines 5,6,10" + ) + })?; + let deadlines_str = args.deadlines.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "SequencingWithReleaseTimesAndDeadlines requires --deadlines\n\n\ + Usage: pred create SequencingWithReleaseTimesAndDeadlines --lengths 3,2,4 --release-times 0,1,5 --deadlines 5,6,10" + ) + })?; + let lengths: Vec = util::parse_comma_list(lengths_str)?; + let release_times: Vec = util::parse_comma_list(release_str)?; + let deadlines: Vec = util::parse_comma_list(deadlines_str)?; + if lengths.len() != release_times.len() || lengths.len() != deadlines.len() { + bail!( + "All three lists must have the same length: lengths={}, release_times={}, deadlines={}", + lengths.len(), + release_times.len(), + deadlines.len() + ); + } + ( + ser(SequencingWithReleaseTimesAndDeadlines::new( + lengths, + release_times, + deadlines, + ))?, + 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 e162efc2..af026853 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -1,6 +1,9 @@ use anyhow::{bail, Context, Result}; use problemreductions::models::algebraic::{ClosestVectorProblem, ILP}; -use problemreductions::models::misc::{BinPacking, Knapsack, LongestCommonSubsequence, SubsetSum}; +use problemreductions::models::misc::{ + BinPacking, Knapsack, LongestCommonSubsequence, SequencingWithReleaseTimesAndDeadlines, + SubsetSum, +}; use problemreductions::prelude::*; use problemreductions::rules::{MinimizeSteps, ReductionGraph}; use problemreductions::solvers::{BruteForce, ILPSolver, Solver}; @@ -249,6 +252,9 @@ pub fn load_problem( "LongestCommonSubsequence" => deser_opt::(data), "MinimumFeedbackVertexSet" => deser_opt::>(data), "SubsetSum" => deser_sat::(data), + "SequencingWithReleaseTimesAndDeadlines" => { + deser_sat::(data) + } _ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)), } } @@ -313,6 +319,9 @@ pub fn serialize_any_problem( "LongestCommonSubsequence" => try_ser::(any), "MinimumFeedbackVertexSet" => try_ser::>(any), "SubsetSum" => try_ser::(any), + "SequencingWithReleaseTimesAndDeadlines" => { + 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 a595f61b..be7949a7 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -58,6 +58,9 @@ pub fn resolve_alias(input: &str) -> String { "lcs" | "longestcommonsubsequence" => "LongestCommonSubsequence".to_string(), "fvs" | "minimumfeedbackvertexset" => "MinimumFeedbackVertexSet".to_string(), "subsetsum" => "SubsetSum".to_string(), + "sequencingwithreleasetimesanddeadlines" => { + "SequencingWithReleaseTimesAndDeadlines".to_string() + } _ => input.to_string(), // pass-through for exact names } } diff --git a/src/lib.rs b/src/lib.rs index ef675413..42a9ab97 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,7 +45,10 @@ pub mod prelude { KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumFeedbackVertexSet, MinimumVertexCover, TravelingSalesman, }; - pub use crate::models::misc::{BinPacking, Factoring, Knapsack, LongestCommonSubsequence, PaintShop, SubsetSum}; + pub use crate::models::misc::{ + BinPacking, Factoring, Knapsack, LongestCommonSubsequence, PaintShop, + SequencingWithReleaseTimesAndDeadlines, 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 943b758a..578de58d 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -6,6 +6,7 @@ //! - [`Knapsack`]: 0-1 Knapsack (maximize value subject to weight capacity) //! - [`LongestCommonSubsequence`]: Longest Common Subsequence //! - [`PaintShop`]: Minimize color switches in paint shop scheduling +//! - [`SequencingWithReleaseTimesAndDeadlines`]: Single-machine scheduling feasibility //! - [`SubsetSum`]: Find a subset summing to exactly a target value mod bin_packing; @@ -13,6 +14,7 @@ pub(crate) mod factoring; mod knapsack; mod longest_common_subsequence; pub(crate) mod paintshop; +mod sequencing_with_release_times_and_deadlines; mod subset_sum; pub use bin_packing::BinPacking; @@ -20,4 +22,5 @@ pub use factoring::Factoring; pub use knapsack::Knapsack; pub use longest_common_subsequence::LongestCommonSubsequence; pub use paintshop::PaintShop; +pub use sequencing_with_release_times_and_deadlines::SequencingWithReleaseTimesAndDeadlines; pub use subset_sum::SubsetSum; diff --git a/src/models/misc/sequencing_with_release_times_and_deadlines.rs b/src/models/misc/sequencing_with_release_times_and_deadlines.rs new file mode 100644 index 00000000..de7d9f4d --- /dev/null +++ b/src/models/misc/sequencing_with_release_times_and_deadlines.rs @@ -0,0 +1,163 @@ +//! Sequencing with Release Times and Deadlines problem implementation. +//! +//! Given a set of tasks each with a processing time, release time, and deadline, +//! determine whether all tasks can be non-preemptively scheduled on one processor +//! such that each task starts after its release time and finishes by its deadline. +//! Strongly NP-complete (Garey & Johnson, A5 SS1). + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "SequencingWithReleaseTimesAndDeadlines", + module_path: module_path!(), + description: "Single-machine scheduling feasibility: can all tasks be scheduled within their release-deadline windows without overlap?", + fields: &[ + FieldInfo { name: "lengths", type_name: "Vec", description: "Processing time l(t) for each task (positive)" }, + FieldInfo { name: "release_times", type_name: "Vec", description: "Release time r(t) for each task (non-negative)" }, + FieldInfo { name: "deadlines", type_name: "Vec", description: "Deadline d(t) for each task (positive)" }, + ], + } +} + +/// Sequencing with Release Times and Deadlines. +/// +/// Given a set of `n` tasks, each with a processing time `l(t)`, release time +/// `r(t)`, and deadline `d(t)`, determine whether there exists a one-processor +/// schedule where each task starts no earlier than its release time and finishes +/// by its deadline, with no two tasks overlapping. +/// +/// # Representation +/// +/// Each variable represents the start time of a task. Variable `i` takes values +/// in `{0, 1, ..., max_deadline - 1}`. A configuration is feasible iff: +/// - `start[i] >= release_times[i]` for all `i` +/// - `start[i] + lengths[i] <= deadlines[i]` for all `i` +/// - No two tasks overlap in time +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::misc::SequencingWithReleaseTimesAndDeadlines; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// let problem = SequencingWithReleaseTimesAndDeadlines::new( +/// vec![1, 2, 1], +/// vec![0, 0, 2], +/// vec![3, 3, 4], +/// ); +/// let solver = BruteForce::new(); +/// let solution = solver.find_satisfying(&problem); +/// assert!(solution.is_some()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SequencingWithReleaseTimesAndDeadlines { + lengths: Vec, + release_times: Vec, + deadlines: Vec, +} + +impl SequencingWithReleaseTimesAndDeadlines { + /// Create a new instance. + /// + /// # Panics + /// + /// Panics if the three vectors have different lengths. + pub fn new(lengths: Vec, release_times: Vec, deadlines: Vec) -> Self { + assert_eq!(lengths.len(), release_times.len()); + assert_eq!(lengths.len(), deadlines.len()); + Self { + lengths, + release_times, + deadlines, + } + } + + /// Returns the processing times. + pub fn lengths(&self) -> &[u64] { + &self.lengths + } + + /// Returns the release times. + pub fn release_times(&self) -> &[u64] { + &self.release_times + } + + /// Returns the deadlines. + pub fn deadlines(&self) -> &[u64] { + &self.deadlines + } + + /// Returns the number of tasks. + pub fn num_tasks(&self) -> usize { + self.lengths.len() + } + + /// Returns the time horizon (maximum deadline). + pub fn time_horizon(&self) -> u64 { + self.deadlines.iter().copied().max().unwrap_or(0) + } +} + +impl Problem for SequencingWithReleaseTimesAndDeadlines { + const NAME: &'static str = "SequencingWithReleaseTimesAndDeadlines"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + let h = self.time_horizon() as usize; + if h == 0 { + return vec![1; self.num_tasks()]; + } + vec![h; self.num_tasks()] + } + + fn evaluate(&self, config: &[usize]) -> bool { + let n = self.num_tasks(); + if config.len() != n { + return false; + } + + // Check each task's release time and deadline constraints + for (i, &start_val) in config.iter().enumerate() { + let start = start_val as u64; + if start < self.release_times[i] { + return false; + } + if start + self.lengths[i] > self.deadlines[i] { + return false; + } + } + + // Check no two tasks overlap: for all i != j, + // either start[i] + length[i] <= start[j] or start[j] + length[j] <= start[i] + for i in 0..n { + for j in (i + 1)..n { + let si = config[i] as u64; + let ei = si + self.lengths[i]; + let sj = config[j] as u64; + let ej = sj + self.lengths[j]; + if ei > sj && ej > si { + return false; + } + } + } + + true + } +} + +impl SatisfactionProblem for SequencingWithReleaseTimesAndDeadlines {} + +crate::declare_variants! { + SequencingWithReleaseTimesAndDeadlines => "2^num_tasks * num_tasks", +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/sequencing_with_release_times_and_deadlines.rs"] +mod tests; diff --git a/src/models/mod.rs b/src/models/mod.rs index 6c8ac38a..eda67d0a 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -16,5 +16,8 @@ pub use graph::{ MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumFeedbackVertexSet, MinimumVertexCover, SpinGlass, TravelingSalesman, }; -pub use misc::{BinPacking, Factoring, Knapsack, LongestCommonSubsequence, PaintShop, SubsetSum}; +pub use misc::{ + BinPacking, Factoring, Knapsack, LongestCommonSubsequence, PaintShop, + SequencingWithReleaseTimesAndDeadlines, SubsetSum, +}; pub use set::{MaximumSetPacking, MinimumSetCovering}; diff --git a/src/rules/longestcommonsubsequence_ilp.rs b/src/rules/longestcommonsubsequence_ilp.rs index a69610a3..4634045b 100644 --- a/src/rules/longestcommonsubsequence_ilp.rs +++ b/src/rules/longestcommonsubsequence_ilp.rs @@ -57,13 +57,7 @@ impl ReductionResult for ReductionLCSToILP { .iter() .enumerate() .filter(|(i, _)| target_solution.get(*i).copied().unwrap_or(0) == 1) - .map(|(_, &(j1, j2))| { - if shortest_is_first { - j1 - } else { - j2 - } - }) + .map(|(_, &(j1, j2))| if shortest_is_first { j1 } else { j2 }) .collect(); let mut config = vec![0usize; shortest_len]; @@ -141,17 +135,11 @@ impl ReduceTo> for LongestCommonSubsequence { for (i, &(j1, j2)) in match_pairs.iter().enumerate() { for (k, &(j1p, j2p)) in match_pairs.iter().enumerate() { if i < k && j1 < j1p && j2 > j2p { - constraints.push(LinearConstraint::le( - vec![(i, 1.0), (k, 1.0)], - 1.0, - )); + constraints.push(LinearConstraint::le(vec![(i, 1.0), (k, 1.0)], 1.0)); } // Also check the reverse: j1 > j1p and j2 < j2p if i < k && j1 > j1p && j2 < j2p { - constraints.push(LinearConstraint::le( - vec![(i, 1.0), (k, 1.0)], - 1.0, - )); + constraints.push(LinearConstraint::le(vec![(i, 1.0), (k, 1.0)], 1.0)); } } } diff --git a/src/unit_tests/models/misc/longest_common_subsequence.rs b/src/unit_tests/models/misc/longest_common_subsequence.rs index e9059dda..33d4a0e9 100644 --- a/src/unit_tests/models/misc/longest_common_subsequence.rs +++ b/src/unit_tests/models/misc/longest_common_subsequence.rs @@ -14,7 +14,10 @@ fn test_lcs_basic() { assert_eq!(problem.num_chars_first(), 6); assert_eq!(problem.num_chars_second(), 6); assert_eq!(problem.direction(), Direction::Maximize); - assert_eq!(::NAME, "LongestCommonSubsequence"); + assert_eq!( + ::NAME, + "LongestCommonSubsequence" + ); assert_eq!(::variant(), vec![]); } @@ -32,10 +35,8 @@ fn test_lcs_dims() { fn test_lcs_evaluate_valid() { // s1 = "ABC", s2 = "ACB" // Selecting positions 0,2 of s1 (shorter) gives "AC" which is subseq of "ACB" - let problem = LongestCommonSubsequence::new(vec![ - vec![b'A', b'B', b'C'], - vec![b'A', b'C', b'B'], - ]); + let problem = + LongestCommonSubsequence::new(vec![vec![b'A', b'B', b'C'], vec![b'A', b'C', b'B']]); let result = problem.evaluate(&[1, 0, 1]); // "AC" assert!(result.is_valid()); assert_eq!(result.unwrap(), 2); @@ -45,20 +46,16 @@ fn test_lcs_evaluate_valid() { fn test_lcs_evaluate_invalid_subsequence() { // s1 = "ABC", s2 = "CAB" // Selecting positions 1,2 of s1 gives "BC" - is "BC" a subseq of "CAB"? No - let problem = LongestCommonSubsequence::new(vec![ - vec![b'A', b'B', b'C'], - vec![b'C', b'A', b'B'], - ]); + let problem = + LongestCommonSubsequence::new(vec![vec![b'A', b'B', b'C'], vec![b'C', b'A', b'B']]); let result = problem.evaluate(&[0, 1, 1]); // "BC" assert!(!result.is_valid()); } #[test] fn test_lcs_evaluate_empty_selection() { - let problem = LongestCommonSubsequence::new(vec![ - vec![b'A', b'B', b'C'], - vec![b'X', b'Y', b'Z'], - ]); + let problem = + LongestCommonSubsequence::new(vec![vec![b'A', b'B', b'C'], vec![b'X', b'Y', b'Z']]); let result = problem.evaluate(&[0, 0, 0]); // empty assert!(result.is_valid()); assert_eq!(result.unwrap(), 0); @@ -66,20 +63,14 @@ fn test_lcs_evaluate_empty_selection() { #[test] fn test_lcs_evaluate_wrong_config_length() { - let problem = LongestCommonSubsequence::new(vec![ - vec![b'A', b'B'], - vec![b'A', b'B', b'C'], - ]); + let problem = LongestCommonSubsequence::new(vec![vec![b'A', b'B'], vec![b'A', b'B', b'C']]); assert!(!problem.evaluate(&[1]).is_valid()); assert!(!problem.evaluate(&[1, 0, 0]).is_valid()); } #[test] fn test_lcs_evaluate_invalid_variable_value() { - let problem = LongestCommonSubsequence::new(vec![ - vec![b'A', b'B'], - vec![b'A', b'B'], - ]); + let problem = LongestCommonSubsequence::new(vec![vec![b'A', b'B'], vec![b'A', b'B']]); assert!(!problem.evaluate(&[2, 0]).is_valid()); } @@ -100,10 +91,8 @@ fn test_lcs_brute_force_two_strings() { #[test] fn test_lcs_identical_strings() { - let problem = LongestCommonSubsequence::new(vec![ - vec![b'A', b'B', b'C'], - vec![b'A', b'B', b'C'], - ]); + let problem = + LongestCommonSubsequence::new(vec![vec![b'A', b'B', b'C'], vec![b'A', b'B', b'C']]); let solver = BruteForce::new(); let solution = solver.find_best(&problem).expect("should find a solution"); let metric = problem.evaluate(&solution); @@ -112,10 +101,7 @@ fn test_lcs_identical_strings() { #[test] fn test_lcs_no_common_chars() { - let problem = LongestCommonSubsequence::new(vec![ - vec![b'A', b'B'], - vec![b'C', b'D'], - ]); + let problem = LongestCommonSubsequence::new(vec![vec![b'A', b'B'], vec![b'C', b'D']]); let solver = BruteForce::new(); let solution = solver.find_best(&problem).expect("should find a solution"); let metric = problem.evaluate(&solution); @@ -125,10 +111,7 @@ fn test_lcs_no_common_chars() { #[test] fn test_lcs_single_char_alphabet() { // All same character - LCS is length of shortest string - let problem = LongestCommonSubsequence::new(vec![ - vec![b'A', b'A', b'A'], - vec![b'A', b'A'], - ]); + let problem = LongestCommonSubsequence::new(vec![vec![b'A', b'A', b'A'], vec![b'A', b'A']]); let solver = BruteForce::new(); let solution = solver.find_best(&problem).expect("should find a solution"); let metric = problem.evaluate(&solution); @@ -152,10 +135,8 @@ fn test_lcs_three_strings() { #[test] fn test_lcs_serialization() { - let problem = LongestCommonSubsequence::new(vec![ - vec![b'A', b'B', b'C'], - vec![b'A', b'C', b'B'], - ]); + let problem = + LongestCommonSubsequence::new(vec![vec![b'A', b'B', b'C'], vec![b'A', b'C', b'B']]); let json = serde_json::to_value(&problem).unwrap(); let restored: LongestCommonSubsequence = serde_json::from_value(json).unwrap(); assert_eq!(restored.strings(), problem.strings()); @@ -163,10 +144,7 @@ fn test_lcs_serialization() { #[test] fn test_lcs_empty_string_in_input() { - let problem = LongestCommonSubsequence::new(vec![ - vec![], - vec![b'A', b'B', b'C'], - ]); + let problem = LongestCommonSubsequence::new(vec![vec![], vec![b'A', b'B', b'C']]); assert_eq!(problem.dims(), Vec::::new()); assert!(problem.evaluate(&[]).is_valid()); assert_eq!(problem.evaluate(&[]).unwrap(), 0); diff --git a/src/unit_tests/models/misc/sequencing_with_release_times_and_deadlines.rs b/src/unit_tests/models/misc/sequencing_with_release_times_and_deadlines.rs new file mode 100644 index 00000000..fcca48c1 --- /dev/null +++ b/src/unit_tests/models/misc/sequencing_with_release_times_and_deadlines.rs @@ -0,0 +1,154 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::traits::Problem; + +#[test] +fn test_sequencing_rtd_basic() { + let problem = SequencingWithReleaseTimesAndDeadlines::new( + vec![3, 2, 4, 1, 2], + vec![0, 1, 5, 0, 8], + vec![5, 6, 10, 3, 12], + ); + assert_eq!(problem.num_tasks(), 5); + assert_eq!(problem.lengths(), &[3, 2, 4, 1, 2]); + assert_eq!(problem.release_times(), &[0, 1, 5, 0, 8]); + assert_eq!(problem.deadlines(), &[5, 6, 10, 3, 12]); + assert_eq!(problem.time_horizon(), 12); + assert_eq!(problem.dims(), vec![12; 5]); + assert_eq!( + ::NAME, + "SequencingWithReleaseTimesAndDeadlines" + ); + assert_eq!( + ::variant(), + vec![] + ); +} + +#[test] +fn test_sequencing_rtd_evaluate_feasible() { + // Example from issue: 5 tasks with a known feasible schedule + let problem = SequencingWithReleaseTimesAndDeadlines::new( + vec![3, 2, 4, 1, 2], + vec![0, 1, 5, 0, 8], + vec![5, 6, 10, 3, 12], + ); + // sigma(t4)=0, sigma(t1)=1, sigma(t2)=4, sigma(t3)=6, sigma(t5)=10 + assert!(problem.evaluate(&[1, 4, 6, 0, 10])); +} + +#[test] +fn test_sequencing_rtd_evaluate_infeasible_deadline() { + let problem = SequencingWithReleaseTimesAndDeadlines::new( + vec![3, 2], + vec![0, 0], + vec![2, 4], // task 0 needs 3 time units but deadline is 2 + ); + // Task 0 starts at 0, finishes at 3 > deadline 2 + assert!(!problem.evaluate(&[0, 3])); +} + +#[test] +fn test_sequencing_rtd_evaluate_infeasible_release() { + let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![1, 1], vec![3, 0], vec![5, 5]); + // Task 0 starts at 0 but release time is 3 + assert!(!problem.evaluate(&[0, 1])); + // Task 0 starts at 3, finishes at 4 <= 5; task 1 starts at 0, finishes at 1 <= 5 + assert!(problem.evaluate(&[3, 0])); +} + +#[test] +fn test_sequencing_rtd_evaluate_overlap() { + let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![2, 2], vec![0, 0], vec![4, 4]); + // Both start at 0, overlap [0,2) and [0,2) + assert!(!problem.evaluate(&[0, 0])); + // Task 0 at [0,2), task 1 at [2,4) — no overlap + assert!(problem.evaluate(&[0, 2])); + // Task 0 at [2,4), task 1 at [0,2) — no overlap + assert!(problem.evaluate(&[2, 0])); + // Task 0 at [1,3), task 1 at [2,4) — overlap at [2,3) + assert!(!problem.evaluate(&[1, 2])); +} + +#[test] +fn test_sequencing_rtd_evaluate_wrong_config_length() { + let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![1, 1], vec![0, 0], vec![2, 2]); + assert!(!problem.evaluate(&[0])); + assert!(!problem.evaluate(&[0, 0, 0])); +} + +#[test] +fn test_sequencing_rtd_empty_instance() { + let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![], vec![], vec![]); + assert_eq!(problem.num_tasks(), 0); + assert_eq!(problem.time_horizon(), 0); + assert_eq!(problem.dims(), Vec::::new()); + assert!(problem.evaluate(&[])); +} + +#[test] +fn test_sequencing_rtd_single_task() { + let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![2], vec![1], vec![5]); + assert_eq!(problem.dims(), vec![5]); + // Start at 1 (release), finish at 3 <= 5 + assert!(problem.evaluate(&[1])); + // Start at 3, finish at 5 <= 5 + assert!(problem.evaluate(&[3])); + // Start at 4, finish at 6 > 5 + assert!(!problem.evaluate(&[4])); + // Start at 0 < release 1 + assert!(!problem.evaluate(&[0])); +} + +#[test] +fn test_sequencing_rtd_brute_force() { + // Small instance: 3 tasks that fit tightly + let problem = + SequencingWithReleaseTimesAndDeadlines::new(vec![1, 2, 1], vec![0, 0, 2], vec![3, 3, 4]); + let solver = BruteForce::new(); + let solution = solver + .find_satisfying(&problem) + .expect("should find a solution"); + assert!(problem.evaluate(&solution)); +} + +#[test] +fn test_sequencing_rtd_brute_force_all() { + let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![1, 1], vec![0, 0], vec![3, 3]); + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + assert!(!solutions.is_empty()); + for sol in &solutions { + assert!(problem.evaluate(sol)); + } +} + +#[test] +fn test_sequencing_rtd_unsatisfiable() { + // Two tasks each need 2 time units but only 3 total time available + let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![2, 2], vec![0, 0], vec![3, 3]); + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!(solution.is_none()); +} + +#[test] +fn test_sequencing_rtd_serialization() { + let problem = + SequencingWithReleaseTimesAndDeadlines::new(vec![3, 2, 4], vec![0, 1, 5], vec![5, 6, 10]); + let json = serde_json::to_value(&problem).unwrap(); + let restored: SequencingWithReleaseTimesAndDeadlines = serde_json::from_value(json).unwrap(); + assert_eq!(restored.lengths(), problem.lengths()); + assert_eq!(restored.release_times(), problem.release_times()); + assert_eq!(restored.deadlines(), problem.deadlines()); +} + +#[test] +fn test_sequencing_rtd_tight_schedule() { + // Tasks that can only be scheduled in one specific order + let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![2, 2], vec![0, 2], vec![2, 4]); + // Only valid: task 0 at [0,2), task 1 at [2,4) + assert!(problem.evaluate(&[0, 2])); + // task 0 at [0,2), task 1 at [1,3) — violates release_time 2 + assert!(!problem.evaluate(&[0, 1])); +} diff --git a/src/unit_tests/rules/longestcommonsubsequence_ilp.rs b/src/unit_tests/rules/longestcommonsubsequence_ilp.rs index 101da6b9..7762d234 100644 --- a/src/unit_tests/rules/longestcommonsubsequence_ilp.rs +++ b/src/unit_tests/rules/longestcommonsubsequence_ilp.rs @@ -9,8 +9,7 @@ fn test_lcs_to_ilp_issue_example() { vec![b'A', b'B', b'A', b'C'], vec![b'B', b'A', b'C', b'A'], ]); - let reduction: ReductionLCSToILP = - ReduceTo::>::reduce_to(&problem); + let reduction: ReductionLCSToILP = ReduceTo::>::reduce_to(&problem); let ilp = reduction.target_problem(); // 6 match pairs as described in the issue @@ -45,8 +44,7 @@ fn test_lcs_to_ilp_closed_loop() { let bf_value = bf_metric.unwrap(); // ILP optimal - let reduction: ReductionLCSToILP = - ReduceTo::>::reduce_to(&problem); + let reduction: ReductionLCSToILP = ReduceTo::>::reduce_to(&problem); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); @@ -66,12 +64,9 @@ fn test_lcs_to_ilp_closed_loop() { #[test] fn test_lcs_to_ilp_identical_strings() { // LCS of identical strings = the string itself - let problem = LongestCommonSubsequence::new(vec![ - vec![b'A', b'B', b'C'], - vec![b'A', b'B', b'C'], - ]); - let reduction: ReductionLCSToILP = - ReduceTo::>::reduce_to(&problem); + let problem = + LongestCommonSubsequence::new(vec![vec![b'A', b'B', b'C'], vec![b'A', b'B', b'C']]); + let reduction: ReductionLCSToILP = ReduceTo::>::reduce_to(&problem); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); @@ -85,12 +80,8 @@ fn test_lcs_to_ilp_identical_strings() { #[test] fn test_lcs_to_ilp_no_common_chars() { - let problem = LongestCommonSubsequence::new(vec![ - vec![b'A', b'B'], - vec![b'C', b'D'], - ]); - let reduction: ReductionLCSToILP = - ReduceTo::>::reduce_to(&problem); + let problem = LongestCommonSubsequence::new(vec![vec![b'A', b'B'], vec![b'C', b'D']]); + let reduction: ReductionLCSToILP = ReduceTo::>::reduce_to(&problem); let ilp = reduction.target_problem(); // No match pairs → 0 variables @@ -108,12 +99,8 @@ fn test_lcs_to_ilp_no_common_chars() { #[test] fn test_lcs_to_ilp_single_char_alphabet() { // All same chars → LCS = min length - let problem = LongestCommonSubsequence::new(vec![ - vec![b'A', b'A', b'A'], - vec![b'A', b'A'], - ]); - let reduction: ReductionLCSToILP = - ReduceTo::>::reduce_to(&problem); + let problem = LongestCommonSubsequence::new(vec![vec![b'A', b'A', b'A'], vec![b'A', b'A']]); + let reduction: ReductionLCSToILP = ReduceTo::>::reduce_to(&problem); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); @@ -128,10 +115,8 @@ fn test_lcs_to_ilp_single_char_alphabet() { #[test] fn test_lcs_to_ilp_asymmetric_lengths() { // s1 = "AB", s2 = "AABB" - let problem = LongestCommonSubsequence::new(vec![ - vec![b'A', b'B'], - vec![b'A', b'A', b'B', b'B'], - ]); + let problem = + LongestCommonSubsequence::new(vec![vec![b'A', b'B'], vec![b'A', b'A', b'B', b'B']]); // BF optimal let bf = BruteForce::new(); @@ -139,8 +124,7 @@ fn test_lcs_to_ilp_asymmetric_lengths() { let bf_value = problem.evaluate(&bf_solution).unwrap(); // ILP optimal - let reduction: ReductionLCSToILP = - ReduceTo::>::reduce_to(&problem); + let reduction: ReductionLCSToILP = ReduceTo::>::reduce_to(&problem); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).unwrap(); @@ -154,12 +138,8 @@ fn test_lcs_to_ilp_asymmetric_lengths() { #[test] fn test_lcs_to_ilp_constraint_structure() { // Verify basic ILP structure for a small example - let problem = LongestCommonSubsequence::new(vec![ - vec![b'A', b'B'], - vec![b'B', b'A'], - ]); - let reduction: ReductionLCSToILP = - ReduceTo::>::reduce_to(&problem); + let problem = LongestCommonSubsequence::new(vec![vec![b'A', b'B'], vec![b'B', b'A']]); + let reduction: ReductionLCSToILP = ReduceTo::>::reduce_to(&problem); let ilp = reduction.target_problem(); // Match pairs: (0,1)=A, (1,0)=B → 2 variables From d003fb5ce90d677838dd840247c85544ff6f2a2c Mon Sep 17 00:00:00 2001 From: zazabap Date: Sun, 15 Mar 2026 14:54:06 +0000 Subject: [PATCH 2/4] merge: resolve conflicts with main Resolve merge conflicts from origin/main: - Accept main's deletion of per-reduction example binaries - Merge registry-based dispatch (main) with PR's model registration - Combine both sides' additive entries in paper, schemas, CLI, and module files - Fix declare_variants! syntax and ProblemSchemaEntry fields to match current API Co-Authored-By: Claude Opus 4.6 --- .../misc/sequencing_with_release_times_and_deadlines.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/models/misc/sequencing_with_release_times_and_deadlines.rs b/src/models/misc/sequencing_with_release_times_and_deadlines.rs index de7d9f4d..4b3ada27 100644 --- a/src/models/misc/sequencing_with_release_times_and_deadlines.rs +++ b/src/models/misc/sequencing_with_release_times_and_deadlines.rs @@ -12,6 +12,9 @@ use serde::{Deserialize, Serialize}; inventory::submit! { ProblemSchemaEntry { name: "SequencingWithReleaseTimesAndDeadlines", + display_name: "Sequencing with Release Times and Deadlines", + aliases: &[], + dimensions: &[], module_path: module_path!(), description: "Single-machine scheduling feasibility: can all tasks be scheduled within their release-deadline windows without overlap?", fields: &[ @@ -155,7 +158,7 @@ impl Problem for SequencingWithReleaseTimesAndDeadlines { impl SatisfactionProblem for SequencingWithReleaseTimesAndDeadlines {} crate::declare_variants! { - SequencingWithReleaseTimesAndDeadlines => "2^num_tasks * num_tasks", + default sat SequencingWithReleaseTimesAndDeadlines => "2^num_tasks * num_tasks", } #[cfg(test)] From abdc1baf87b9d7f3d715e85875431a7ff23f71d1 Mon Sep 17 00:00:00 2001 From: zazabap Date: Sun, 15 Mar 2026 14:56:24 +0000 Subject: [PATCH 3/4] refactor: use permutation encoding (Lehmer code) for SequencingWithReleaseTimesAndDeadlines Switch from start-time encoding to permutation encoding consistent with MinimumTardinessSequencing, as recommended in issue #494 review. This: - Uses dims = [n, n-1, ..., 2, 1] instead of [max_deadline; n] - Eliminates overflow concerns (no u64 arithmetic on config values) - Dramatically reduces brute-force search space - Schedules tasks left-to-right with greedy start times Addresses Copilot review comments about overflow protection and inflated search space. Co-Authored-By: Claude Opus 4.6 --- ...encing_with_release_times_and_deadlines.rs | 49 ++++++------- ...encing_with_release_times_and_deadlines.rs | 70 ++++++++----------- 2 files changed, 51 insertions(+), 68 deletions(-) diff --git a/src/models/misc/sequencing_with_release_times_and_deadlines.rs b/src/models/misc/sequencing_with_release_times_and_deadlines.rs index 4b3ada27..7b50ea13 100644 --- a/src/models/misc/sequencing_with_release_times_and_deadlines.rs +++ b/src/models/misc/sequencing_with_release_times_and_deadlines.rs @@ -34,11 +34,11 @@ inventory::submit! { /// /// # Representation /// -/// Each variable represents the start time of a task. Variable `i` takes values -/// in `{0, 1, ..., max_deadline - 1}`. A configuration is feasible iff: -/// - `start[i] >= release_times[i]` for all `i` -/// - `start[i] + lengths[i] <= deadlines[i]` for all `i` -/// - No two tasks overlap in time +/// Uses a permutation encoding (Lehmer code), where `config[i]` selects which +/// remaining task to schedule next from the pool of unscheduled tasks. +/// `dims() = [n, n-1, ..., 2, 1]`. Tasks are scheduled left-to-right: each +/// task starts at `max(release_time, current_time)`. The schedule is feasible +/// iff every task finishes by its deadline. /// /// # Example /// @@ -113,11 +113,8 @@ impl Problem for SequencingWithReleaseTimesAndDeadlines { } fn dims(&self) -> Vec { - let h = self.time_horizon() as usize; - if h == 0 { - return vec![1; self.num_tasks()]; - } - vec![h; self.num_tasks()] + let n = self.num_tasks(); + (0..n).rev().map(|i| i + 1).collect() } fn evaluate(&self, config: &[usize]) -> bool { @@ -126,29 +123,25 @@ impl Problem for SequencingWithReleaseTimesAndDeadlines { return false; } - // Check each task's release time and deadline constraints - for (i, &start_val) in config.iter().enumerate() { - let start = start_val as u64; - if start < self.release_times[i] { - return false; - } - if start + self.lengths[i] > self.deadlines[i] { + // Decode Lehmer code into a permutation of task indices. + let mut available: Vec = (0..n).collect(); + let mut schedule = Vec::with_capacity(n); + for &c in config.iter() { + if c >= available.len() { return false; } + schedule.push(available.remove(c)); } - // Check no two tasks overlap: for all i != j, - // either start[i] + length[i] <= start[j] or start[j] + length[j] <= start[i] - for i in 0..n { - for j in (i + 1)..n { - let si = config[i] as u64; - let ei = si + self.lengths[i]; - let sj = config[j] as u64; - let ej = sj + self.lengths[j]; - if ei > sj && ej > si { - return false; - } + // Schedule tasks left-to-right: each task starts at max(release_time, current_time). + let mut current_time: u64 = 0; + for &task in &schedule { + let start = current_time.max(self.release_times[task]); + let finish = start + self.lengths[task]; + if finish > self.deadlines[task] { + return false; } + current_time = finish; } true diff --git a/src/unit_tests/models/misc/sequencing_with_release_times_and_deadlines.rs b/src/unit_tests/models/misc/sequencing_with_release_times_and_deadlines.rs index fcca48c1..f3449da4 100644 --- a/src/unit_tests/models/misc/sequencing_with_release_times_and_deadlines.rs +++ b/src/unit_tests/models/misc/sequencing_with_release_times_and_deadlines.rs @@ -14,7 +14,8 @@ fn test_sequencing_rtd_basic() { assert_eq!(problem.release_times(), &[0, 1, 5, 0, 8]); assert_eq!(problem.deadlines(), &[5, 6, 10, 3, 12]); assert_eq!(problem.time_horizon(), 12); - assert_eq!(problem.dims(), vec![12; 5]); + // Lehmer code dims: [5, 4, 3, 2, 1] + assert_eq!(problem.dims(), vec![5, 4, 3, 2, 1]); assert_eq!( ::NAME, "SequencingWithReleaseTimesAndDeadlines" @@ -27,14 +28,22 @@ fn test_sequencing_rtd_basic() { #[test] fn test_sequencing_rtd_evaluate_feasible() { - // Example from issue: 5 tasks with a known feasible schedule + // 5 tasks: schedule order t3, t0, t1, t2, t4 + // t3: start=max(0,0)=0, finish=1 <= 3 ✓ + // t0: start=max(0,1)=1, finish=4 <= 5 ✓ + // t1: start=max(1,4)=4, finish=6 <= 6 ✓ + // t2: start=max(5,6)=6, finish=10 <= 10 ✓ + // t4: start=max(8,10)=10, finish=12 <= 12 ✓ let problem = SequencingWithReleaseTimesAndDeadlines::new( vec![3, 2, 4, 1, 2], vec![0, 1, 5, 0, 8], vec![5, 6, 10, 3, 12], ); - // sigma(t4)=0, sigma(t1)=1, sigma(t2)=4, sigma(t3)=6, sigma(t5)=10 - assert!(problem.evaluate(&[1, 4, 6, 0, 10])); + // Lehmer code for permutation [3, 0, 1, 2, 4]: + // available=[0,1,2,3,4], pick 3 -> index 3; available=[0,1,2,4], pick 0 -> index 0; + // available=[1,2,4], pick 1 -> index 0; available=[2,4], pick 2 -> index 0; + // available=[4], pick 4 -> index 0 + assert!(problem.evaluate(&[3, 0, 0, 0, 0])); } #[test] @@ -44,30 +53,10 @@ fn test_sequencing_rtd_evaluate_infeasible_deadline() { vec![0, 0], vec![2, 4], // task 0 needs 3 time units but deadline is 2 ); - // Task 0 starts at 0, finishes at 3 > deadline 2 - assert!(!problem.evaluate(&[0, 3])); -} - -#[test] -fn test_sequencing_rtd_evaluate_infeasible_release() { - let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![1, 1], vec![3, 0], vec![5, 5]); - // Task 0 starts at 0 but release time is 3 - assert!(!problem.evaluate(&[0, 1])); - // Task 0 starts at 3, finishes at 4 <= 5; task 1 starts at 0, finishes at 1 <= 5 - assert!(problem.evaluate(&[3, 0])); -} - -#[test] -fn test_sequencing_rtd_evaluate_overlap() { - let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![2, 2], vec![0, 0], vec![4, 4]); - // Both start at 0, overlap [0,2) and [0,2) + // Order [0, 1]: t0 start=0, finish=3 > 2 -> infeasible assert!(!problem.evaluate(&[0, 0])); - // Task 0 at [0,2), task 1 at [2,4) — no overlap - assert!(problem.evaluate(&[0, 2])); - // Task 0 at [2,4), task 1 at [0,2) — no overlap - assert!(problem.evaluate(&[2, 0])); - // Task 0 at [1,3), task 1 at [2,4) — overlap at [2,3) - assert!(!problem.evaluate(&[1, 2])); + // Order [1, 0]: t1 start=0, finish=2; t0 start=2, finish=5 > 2 -> infeasible + assert!(!problem.evaluate(&[1, 0])); } #[test] @@ -89,15 +78,9 @@ fn test_sequencing_rtd_empty_instance() { #[test] fn test_sequencing_rtd_single_task() { let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![2], vec![1], vec![5]); - assert_eq!(problem.dims(), vec![5]); - // Start at 1 (release), finish at 3 <= 5 - assert!(problem.evaluate(&[1])); - // Start at 3, finish at 5 <= 5 - assert!(problem.evaluate(&[3])); - // Start at 4, finish at 6 > 5 - assert!(!problem.evaluate(&[4])); - // Start at 0 < release 1 - assert!(!problem.evaluate(&[0])); + assert_eq!(problem.dims(), vec![1]); + // Only one permutation: task 0 starts at max(1,0)=1, finish=3 <= 5 + assert!(problem.evaluate(&[0])); } #[test] @@ -147,8 +130,15 @@ fn test_sequencing_rtd_serialization() { fn test_sequencing_rtd_tight_schedule() { // Tasks that can only be scheduled in one specific order let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![2, 2], vec![0, 2], vec![2, 4]); - // Only valid: task 0 at [0,2), task 1 at [2,4) - assert!(problem.evaluate(&[0, 2])); - // task 0 at [0,2), task 1 at [1,3) — violates release_time 2 - assert!(!problem.evaluate(&[0, 1])); + // Order [0, 1]: t0 start=max(0,0)=0, finish=2<=2; t1 start=max(2,2)=2, finish=4<=4 ✓ + assert!(problem.evaluate(&[0, 0])); + // Order [1, 0]: t1 start=max(2,0)=2, finish=4<=4; t0 start=max(0,4)=4, finish=6>2 ✗ + assert!(!problem.evaluate(&[1, 0])); +} + +#[test] +fn test_sequencing_rtd_invalid_lehmer_index() { + let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![1, 1], vec![0, 0], vec![2, 2]); + // config[0]=2 is out of range for available.len()=2 + assert!(!problem.evaluate(&[2, 0])); } From 1281ddb43bc2c710491c7862f012631a059dee32 Mon Sep 17 00:00:00 2001 From: zazabap Date: Sun, 15 Mar 2026 15:00:11 +0000 Subject: [PATCH 4/4] fix: add canonical example and trait_consistency entry for SequencingWithReleaseTimesAndDeadlines Add missing structural completeness items: - Canonical model example in example-db (Lehmer code [3,0,0,0,0] for 5-task instance) - trait_consistency test entry Co-Authored-By: Claude Opus 4.6 --- src/models/misc/mod.rs | 1 + ...uencing_with_release_times_and_deadlines.rs | 18 ++++++++++++++++++ src/unit_tests/trait_consistency.rs | 4 ++++ 3 files changed, 23 insertions(+) diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index 89dc005f..8d4dd76b 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -41,5 +41,6 @@ pub(crate) fn canonical_model_example_specs() -> Vec "2^num_tasks * num_tasks", } +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "sequencing_with_release_times_and_deadlines", + build: || { + // 5 tasks from issue example. + // Feasible schedule order: t3, t0, t1, t2, t4 + let problem = SequencingWithReleaseTimesAndDeadlines::new( + vec![3, 2, 4, 1, 2], + vec![0, 1, 5, 0, 8], + vec![5, 6, 10, 3, 12], + ); + // Lehmer code [3,0,0,0,0] = permutation [3,0,1,2,4] + crate::example_db::specs::satisfaction_example(problem, vec![vec![3, 0, 0, 0, 0]]) + }, + }] +} + #[cfg(test)] #[path = "../../unit_tests/models/misc/sequencing_with_release_times_and_deadlines.rs"] mod tests; diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index eee5dc64..cba05e0d 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -130,6 +130,10 @@ fn test_all_problems_implement_trait_correctly() { &MinimumTardinessSequencing::new(3, vec![2, 3, 1], vec![(0, 2)]), "MinimumTardinessSequencing", ); + check_problem_trait( + &SequencingWithReleaseTimesAndDeadlines::new(vec![1, 2, 1], vec![0, 0, 2], vec![3, 3, 4]), + "SequencingWithReleaseTimesAndDeadlines", + ); } #[test]