diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 4910b9b4f..f705d22d3 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -109,6 +109,7 @@ "PartitionIntoTriangles": [Partition Into Triangles], "FlowShopScheduling": [Flow Shop Scheduling], "MinimumTardinessSequencing": [Minimum Tardiness Sequencing], + "SequencingWithinIntervals": [Sequencing Within Intervals], "DirectedTwoCommodityIntegralFlow": [Directed Two-Commodity Integral Flow], ) @@ -2057,6 +2058,100 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS ) ] +#{ + let x = load-model-example("SequencingWithinIntervals") + let ntasks = x.instance.lengths.len() + let release = x.instance.release_times + let deadline = x.instance.deadlines + let lengths = x.instance.lengths + let sol = x.optimal.at(0) + // Compute start times from config offsets: start_i = release_i + config_i + let starts = range(ntasks).map(i => release.at(i) + sol.config.at(i)) + // Identify the enforcer task: the one with the tightest window (deadline - release == length) + let enforcer = range(ntasks).filter(i => deadline.at(i) - release.at(i) == lengths.at(i)).at(0) + let regular = range(ntasks).filter(i => i != enforcer) + // Partition sum B = total length of regular tasks + let B = regular.map(i => lengths.at(i)).sum() + [ + #problem-def("SequencingWithinIntervals")[ + Given a finite set $T$ of tasks and, for each $t in T$, a release time $r(t) >= 0$, a deadline $d(t) >= 0$, and a processing length $ell(t) in ZZ^+$ satisfying $r(t) + ell(t) <= d(t)$, determine whether there exists a feasible schedule $sigma: T -> ZZ_(>= 0)$ such that for each $t in T$: (1) $sigma(t) >= r(t)$, (2) $sigma(t) + ell(t) <= d(t)$, and (3) for all $t' in T backslash {t}$, either $sigma(t') + ell(t') <= sigma(t)$ or $sigma(t') >= sigma(t) + ell(t)$. + ][ + Sequencing Within Intervals is problem SS1 in Garey & Johnson @garey1979, proved NP-complete via reduction from Partition (Theorem 3.8). Each task $t$ must execute non-preemptively during the interval $[r(t), d(t))$, occupying $ell(t)$ consecutive time units, and no two tasks may overlap. The problem is a canonical single-machine scheduling problem and one of the earliest NP-completeness results for scheduling theory. + + The NP-completeness proof uses an "enforcer" task pinned at the midpoint of the time horizon, forcing the remaining tasks to split into two balanced groups --- directly encoding the Partition problem. + + *Example.* Consider #ntasks tasks derived from a Partition instance with $A = {#regular.map(i => str(lengths.at(i))).join(", ")}$ (sum $B = #B$): + #align(center, table( + columns: ntasks + 1, + align: center, + table.header([$"Task"$], ..regular.map(i => [$t_#(i + 1)$]), [$overline(t)$]), + [$r(t)$], ..regular.map(i => [#release.at(i)]), [#release.at(enforcer)], + [$d(t)$], ..regular.map(i => [#deadline.at(i)]), [#deadline.at(enforcer)], + [$ell(t)$], ..regular.map(i => [#lengths.at(i)]), [#lengths.at(enforcer)], + )) + The enforcer task $overline(t)$ must run in $[#release.at(enforcer), #deadline.at(enforcer))$, splitting the schedule into $[0, #release.at(enforcer))$ and $[#deadline.at(enforcer), #deadline.at(0))$. Each side has #(B / 2) time units, and tasks with total length $#(B / 2)$ must fill each side --- corresponding to a partition of $A$. + + #figure( + canvas(length: 1cm, { + import draw: * + let colors = (rgb("#4e79a7"), rgb("#e15759"), rgb("#76b7b2"), rgb("#f28e2b")) + let enforcer-color = rgb("#b07aa1") + let task-labels = regular.map(i => "$t_" + str(i + 1) + "$") + ("$overline(t)$",) + let task-order = regular + (enforcer,) + let scale = 0.7 + let row-h = 0.6 + + // Single-row Gantt chart: all tasks on one timeline + for (k, i) in task-order.enumerate() { + let s = starts.at(i) + let e = s + lengths.at(i) + let x0 = s * scale + let x1 = e * scale + let col = if i == enforcer { enforcer-color } else { colors.at(regular.position(j => j == i)) } + rect((x0, -row-h / 2), (x1, row-h / 2), + fill: col.transparentize(30%), stroke: 0.4pt + col) + content(((x0 + x1) / 2, 0), text(6pt, task-labels.at(k))) + } + + // Release-time and deadline markers for each task + for (k, i) in task-order.enumerate() { + let col = if i == enforcer { enforcer-color } else { colors.at(regular.position(j => j == i)) } + // Release time: upward triangle below axis + let rx = release.at(i) * scale + line((rx, -row-h / 2 - 0.05), (rx, -row-h / 2 - 0.18), stroke: 0.5pt + col) + // Deadline: downward tick above axis + let dx = deadline.at(i) * scale + line((dx, row-h / 2 + 0.05), (dx, row-h / 2 + 0.18), stroke: 0.5pt + col) + } + + // Release / deadline group labels + content((-0.5, -row-h / 2 - 0.12), text(5pt)[$r$]) + content((-0.5, row-h / 2 + 0.12), text(5pt)[$d$]) + + // Time axis + let max-t = 11 + let y-axis = -row-h / 2 - 0.35 + line((0, y-axis), (max-t * scale, y-axis), stroke: 0.4pt) + for t in range(max-t + 1) { + let x = t * scale + line((x, y-axis), (x, y-axis - 0.08), stroke: 0.4pt) + if calc.rem(t, 2) == 0 or t == max-t { + content((x, y-axis - 0.22), text(5pt, str(t))) + } + } + content((max-t * scale / 2, y-axis - 0.45), text(7pt)[$t$]) + + // Enforcer region highlight + let ex0 = release.at(enforcer) * scale + let ex1 = deadline.at(enforcer) * scale + line((ex0, row-h / 2 + 0.3), (ex0, y-axis), stroke: (paint: enforcer-color, thickness: 0.6pt, dash: "dashed")) + line((ex1, row-h / 2 + 0.3), (ex1, y-axis), stroke: (paint: enforcer-color, thickness: 0.6pt, dash: "dashed")) + }), + caption: [Feasible schedule for the SWI instance. The enforcer task $overline(t)$ (purple) is pinned at $[#release.at(enforcer), #deadline.at(enforcer))$, splitting the timeline into two halves of #(B / 2) time units each.], + ) + ] + ] +} #{ let x = load-model-example("MinimumTardinessSequencing") let ntasks = x.instance.num_tasks diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 4f0ff53e3..75d619cc6 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -238,6 +238,7 @@ Flags by problem type: BMF --matrix (0/1), --rank SteinerTree --graph, --edge-weights, --terminals CVP --basis, --target-vec [--bounds] + SequencingWithinIntervals --release-times, --deadlines, --lengths OptimalLinearArrangement --graph, --bound RuralPostman (RPP) --graph, --edge-weights, --required-edges, --bound MultipleChoiceBranching --arcs [--weights] --partition --bound [--num-vertices] @@ -409,6 +410,12 @@ pub struct CreateArgs { /// Variable bounds for CVP as "lower,upper" (e.g., "-10,10") [default: -10,10] #[arg(long, allow_hyphen_values = true)] pub bounds: Option, + /// Release times for SequencingWithinIntervals (comma-separated, e.g., "0,0,5") + #[arg(long)] + pub release_times: Option, + /// Processing lengths for SequencingWithinIntervals (comma-separated, e.g., "3,1,1") + #[arg(long)] + pub lengths: Option, /// Terminal vertices for SteinerTree (comma-separated indices, e.g., "0,2,4") #[arg(long)] pub terminals: Option, diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 47e5c682f..c61ab5ac2 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -8,10 +8,11 @@ use problemreductions::export::{ModelExample, ProblemRef, ProblemSide, RuleExamp use problemreductions::models::algebraic::{ClosestVectorProblem, BMF}; use problemreductions::models::graph::{ GraphPartitioning, HamiltonianPath, LengthBoundedDisjointPaths, MultipleChoiceBranching, + SteinerTree, }; use problemreductions::models::misc::{ BinPacking, FlowShopScheduling, LongestCommonSubsequence, MinimumTardinessSequencing, - PaintShop, ShortestCommonSupersequence, SubsetSum, + PaintShop, SequencingWithinIntervals, ShortestCommonSupersequence, SubsetSum, }; use problemreductions::prelude::*; use problemreductions::registry::collect_schemas; @@ -64,6 +65,9 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.basis.is_none() && args.target_vec.is_none() && args.bounds.is_none() + && args.release_times.is_none() + && args.deadlines.is_none() + && args.lengths.is_none() && args.terminals.is_none() && args.tree.is_none() && args.required_edges.is_none() @@ -227,6 +231,7 @@ fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { "Vec>" => "semicolon-separated groups: \"0,1;2,3\"", "usize" => "integer", "u64" => "integer", + "Vec" => "comma-separated integers: 0,0,5", "i64" => "integer", "BigUint" => "nonnegative decimal integer", "Vec" => "comma-separated nonnegative decimal integers: 3,7,1,8", @@ -285,6 +290,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { } "PartitionIntoTriangles" => "--graph 0-1,1-2,0-2", "Factoring" => "--target 15 --m 4 --n 4", + "SequencingWithinIntervals" => "--release-times 0,0,5 --deadlines 11,11,6 --lengths 3,1,1", "SteinerTree" => "--graph 0-1,1-2,1-3,3-4 --edge-weights 2,2,1,1 --terminals 0,2,4", "OptimalLinearArrangement" => "--graph 0-1,1-2,2-3 --bound 5", "DirectedTwoCommodityIntegralFlow" => { @@ -1123,6 +1129,31 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // SequencingWithinIntervals + "SequencingWithinIntervals" => { + let usage = "Usage: pred create SequencingWithinIntervals --release-times 0,0,5 --deadlines 11,11,6 --lengths 3,1,1"; + let rt_str = args.release_times.as_deref().ok_or_else(|| { + anyhow::anyhow!("SequencingWithinIntervals requires --release-times\n\n{usage}") + })?; + let dl_str = args.deadlines.as_deref().ok_or_else(|| { + anyhow::anyhow!("SequencingWithinIntervals requires --deadlines\n\n{usage}") + })?; + let len_str = args.lengths.as_deref().ok_or_else(|| { + anyhow::anyhow!("SequencingWithinIntervals requires --lengths\n\n{usage}") + })?; + let release_times: Vec = util::parse_comma_list(rt_str)?; + let deadlines: Vec = util::parse_comma_list(dl_str)?; + let lengths: Vec = util::parse_comma_list(len_str)?; + ( + ser(SequencingWithinIntervals::new( + release_times, + deadlines, + lengths, + ))?, + resolved_variant.clone(), + ) + } + // OptimalLinearArrangement — graph + bound "OptimalLinearArrangement" => { let (graph, _) = parse_graph(args).map_err(|e| { diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 70f6ab838..8bce3db9a 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -4562,3 +4562,76 @@ fn test_create_weighted_mis_round_trips_into_solve() { let json: serde_json::Value = serde_json::from_str(&stdout).unwrap(); assert_eq!(json["evaluation"], "Valid(5)"); } + +#[test] +fn test_create_sequencing_within_intervals() { + let output_file = + std::env::temp_dir().join("pred_test_create_sequencing_within_intervals.json"); + let output = pred() + .args([ + "-o", + output_file.to_str().unwrap(), + "create", + "SequencingWithinIntervals", + "--release-times", + "0,0,0,0,5", + "--deadlines", + "11,11,11,11,6", + "--lengths", + "3,1,2,4,1", + ]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let content = std::fs::read_to_string(&output_file).unwrap(); + let json: serde_json::Value = serde_json::from_str(&content).unwrap(); + assert_eq!(json["type"], "SequencingWithinIntervals"); + assert_eq!( + json["data"]["release_times"], + serde_json::json!([0, 0, 0, 0, 5]) + ); + assert_eq!( + json["data"]["deadlines"], + serde_json::json!([11, 11, 11, 11, 6]) + ); + assert_eq!(json["data"]["lengths"], serde_json::json!([3, 1, 2, 4, 1])); + std::fs::remove_file(&output_file).ok(); +} + +#[test] +fn test_create_model_example_sequencing_within_intervals() { + let output = pred() + .args(["create", "--example", "SequencingWithinIntervals"]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout).unwrap(); + let json: serde_json::Value = serde_json::from_str(&stdout).unwrap(); + assert_eq!(json["type"], "SequencingWithinIntervals"); +} + +#[test] +fn test_create_sequencing_within_intervals_rejects_empty_window() { + let output = pred() + .args([ + "create", + "SequencingWithinIntervals", + "--release-times", + "5", + "--deadlines", + "3", + "--lengths", + "2", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); +} diff --git a/src/example_db/fixtures/examples.json b/src/example_db/fixtures/examples.json index ea07c1daa..cb3d84c48 100644 --- a/src/example_db/fixtures/examples.json +++ b/src/example_db/fixtures/examples.json @@ -31,6 +31,7 @@ {"problem":"PartitionIntoTriangles","variant":{"graph":"SimpleGraph"},"instance":{"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[1,2,null],[3,4,null],[3,5,null],[4,5,null],[0,3,null]],"node_holes":[],"nodes":[null,null,null,null,null,null]}}},"samples":[{"config":[0,0,0,1,1,1],"metric":true}],"optimal":[{"config":[0,0,0,1,1,1],"metric":true},{"config":[1,1,1,0,0,0],"metric":true}]}, {"problem":"QUBO","variant":{"weight":"f64"},"instance":{"matrix":[[-1.0,2.0,0.0],[0.0,-1.0,2.0],[0.0,0.0,-1.0]],"num_vars":3},"samples":[{"config":[1,0,1],"metric":{"Valid":-2.0}}],"optimal":[{"config":[1,0,1],"metric":{"Valid":-2.0}}]}, {"problem":"Satisfiability","variant":{},"instance":{"clauses":[{"literals":[1,2]},{"literals":[-1,3]},{"literals":[-2,-3]}],"num_vars":3},"samples":[{"config":[1,0,1],"metric":true}],"optimal":[{"config":[0,1,0],"metric":true},{"config":[1,0,1],"metric":true}]}, + {"problem":"SequencingWithinIntervals","variant":{},"instance":{"deadlines":[11,11,11,11,6],"lengths":[3,1,2,4,1],"release_times":[0,0,0,0,5]},"samples":[{"config":[0,6,3,7,0],"metric":true}],"optimal":[{"config":[0,6,3,7,0],"metric":true},{"config":[0,10,3,6,0],"metric":true},{"config":[2,6,0,7,0],"metric":true},{"config":[2,10,0,6,0],"metric":true},{"config":[6,0,9,1,0],"metric":true},{"config":[6,4,9,0,0],"metric":true},{"config":[8,0,6,1,0],"metric":true},{"config":[8,4,6,0,0],"metric":true}]}, {"problem":"SetBasis","variant":{},"instance":{"collection":[[0,1],[1,2],[0,2],[0,1,2]],"k":3,"universe_size":4},"samples":[{"config":[1,0,0,0,0,1,0,0,0,0,1,0],"metric":true}],"optimal":[{"config":[0,0,1,0,0,1,0,0,1,0,0,0],"metric":true},{"config":[0,0,1,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,1,0,0,0,0,1,0,1,0,0,0],"metric":true},{"config":[0,1,0,0,1,0,0,0,0,0,1,0],"metric":true},{"config":[0,1,1,0,1,0,1,0,1,1,0,0],"metric":true},{"config":[0,1,1,0,1,1,0,0,1,0,1,0],"metric":true},{"config":[1,0,0,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,0,0,0,0,1,0,0,0,0,1,0],"metric":true},{"config":[1,0,1,0,0,1,1,0,1,1,0,0],"metric":true},{"config":[1,0,1,0,1,1,0,0,0,1,1,0],"metric":true},{"config":[1,1,0,0,0,1,1,0,1,0,1,0],"metric":true},{"config":[1,1,0,0,1,0,1,0,0,1,1,0],"metric":true}]}, {"problem":"ShortestCommonSupersequence","variant":{},"instance":{"alphabet_size":3,"bound":4,"strings":[[0,1,2],[1,0,2]]},"samples":[{"config":[1,0,1,2],"metric":true}],"optimal":[{"config":[0,1,0,2],"metric":true},{"config":[1,0,1,2],"metric":true}]}, {"problem":"SpinGlass","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"couplings":[1,1,1,1,1,1,1],"fields":[0,0,0,0,0],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[1,2,null],[3,4,null],[0,3,null],[1,3,null],[1,4,null],[2,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}}},"samples":[{"config":[1,0,1,1,0],"metric":{"Valid":-3}}],"optimal":[{"config":[0,0,1,1,0],"metric":{"Valid":-3}},{"config":[0,1,0,0,1],"metric":{"Valid":-3}},{"config":[0,1,0,1,0],"metric":{"Valid":-3}},{"config":[0,1,1,1,0],"metric":{"Valid":-3}},{"config":[1,0,0,0,1],"metric":{"Valid":-3}},{"config":[1,0,1,0,1],"metric":{"Valid":-3}},{"config":[1,0,1,1,0],"metric":{"Valid":-3}},{"config":[1,1,0,0,1],"metric":{"Valid":-3}}]}, diff --git a/src/lib.rs b/src/lib.rs index dd1642522..f7f7b47ea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,7 +58,8 @@ pub mod prelude { }; pub use crate::models::misc::{ BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, - MinimumTardinessSequencing, PaintShop, ShortestCommonSupersequence, SubsetSum, + MinimumTardinessSequencing, PaintShop, SequencingWithinIntervals, + ShortestCommonSupersequence, SubsetSum, }; pub use crate::models::set::{ ExactCoverBy3Sets, MaximumSetPacking, MinimumSetCovering, SetBasis, diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index cc96aa83e..c4b125274 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -8,6 +8,7 @@ //! - [`LongestCommonSubsequence`]: Longest Common Subsequence //! - [`MinimumTardinessSequencing`]: Minimize tardy tasks in single-machine scheduling //! - [`PaintShop`]: Minimize color switches in paint shop scheduling +//! - [`SequencingWithinIntervals`]: Schedule tasks within time windows //! - [`ShortestCommonSupersequence`]: Find a common supersequence of bounded length //! - [`SubsetSum`]: Find a subset summing to exactly a target value @@ -18,6 +19,7 @@ mod knapsack; mod longest_common_subsequence; mod minimum_tardiness_sequencing; pub(crate) mod paintshop; +mod sequencing_within_intervals; pub(crate) mod shortest_common_supersequence; mod subset_sum; @@ -28,6 +30,7 @@ pub use knapsack::Knapsack; pub use longest_common_subsequence::LongestCommonSubsequence; pub use minimum_tardiness_sequencing::MinimumTardinessSequencing; pub use paintshop::PaintShop; +pub use sequencing_within_intervals::SequencingWithinIntervals; pub use shortest_common_supersequence::ShortestCommonSupersequence; pub use subset_sum::SubsetSum; @@ -36,6 +39,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec", description: "Release time r(t) for each task" }, + FieldInfo { name: "deadlines", type_name: "Vec", description: "Deadline d(t) for each task" }, + FieldInfo { name: "lengths", type_name: "Vec", description: "Processing length l(t) for each task" }, + ], + } +} + +/// Sequencing Within Intervals problem. +/// +/// Given `n` tasks, each with release time `r(t)`, deadline `d(t)`, and processing +/// length `l(t)`, determine whether there exists a schedule `sigma: T -> Z_>=0` +/// such that: +/// - `sigma(t) >= r(t)` (task starts no earlier than its release time) +/// - `sigma(t) + l(t) <= d(t)` (task finishes by its deadline) +/// - No two tasks overlap in time +/// +/// This is problem SS1 from Garey & Johnson (1979), NP-complete via Theorem 3.8. +/// +/// # Representation +/// +/// Each task has a variable representing its start time offset from the release time. +/// Variable `i` takes values in `{0, ..., d(i) - r(i) - l(i)}`, so the actual start +/// time is `r(i) + config[i]`. +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::misc::SequencingWithinIntervals; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// // 3 tasks: release_times = [0, 2, 4], deadlines = [3, 5, 7], lengths = [2, 2, 2] +/// let problem = SequencingWithinIntervals::new(vec![0, 2, 4], vec![3, 5, 7], vec![2, 2, 2]); +/// let solver = BruteForce::new(); +/// let solution = solver.find_satisfying(&problem); +/// assert!(solution.is_some()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SequencingWithinIntervals { + /// Release times for each task. + release_times: Vec, + /// Deadlines for each task. + deadlines: Vec, + /// Processing lengths for each task. + lengths: Vec, +} + +impl SequencingWithinIntervals { + /// Create a new SequencingWithinIntervals problem. + /// + /// # Panics + /// Panics if the three vectors have different lengths, or if any task has + /// `r(i) + l(i) > d(i)` (empty time window). + pub fn new(release_times: Vec, deadlines: Vec, lengths: Vec) -> Self { + assert_eq!( + release_times.len(), + deadlines.len(), + "release_times and deadlines must have the same length" + ); + assert_eq!( + release_times.len(), + lengths.len(), + "release_times and lengths must have the same length" + ); + for i in 0..release_times.len() { + let sum = release_times[i] + .checked_add(lengths[i]) + .expect("overflow computing r(i) + l(i)"); + assert!( + sum <= deadlines[i], + "Task {i}: r({}) + l({}) > d({}), time window is empty", + release_times[i], + lengths[i], + deadlines[i] + ); + } + Self { + release_times, + deadlines, + 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 processing lengths. + pub fn lengths(&self) -> &[u64] { + &self.lengths + } + + /// Returns the number of tasks. + pub fn num_tasks(&self) -> usize { + self.release_times.len() + } +} + +impl Problem for SequencingWithinIntervals { + const NAME: &'static str = "SequencingWithinIntervals"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + (0..self.num_tasks()) + .map(|i| (self.deadlines[i] - self.release_times[i] - self.lengths[i] + 1) as usize) + .collect() + } + + fn evaluate(&self, config: &[usize]) -> bool { + let n = self.num_tasks(); + if config.len() != n { + return false; + } + + // Check each variable is within range and compute start times + let mut starts = Vec::with_capacity(n); + for (i, &c) in config.iter().enumerate() { + let dim = (self.deadlines[i] - self.release_times[i] - self.lengths[i] + 1) as usize; + if c >= dim { + return false; + } + // start = r[i] + c, and c < dim = d[i] - r[i] - l[i] + 1, + // so start + l[i] <= d[i] is guaranteed by construction. + let start = self.release_times[i] + c as u64; + starts.push(start); + } + + // Check no two tasks overlap + for i in 0..n { + for j in (i + 1)..n { + let end_i = starts[i] + self.lengths[i]; + let end_j = starts[j] + self.lengths[j]; + // Tasks overlap if neither finishes before the other starts + if !(end_i <= starts[j] || end_j <= starts[i]) { + return false; + } + } + } + + true + } +} + +impl SatisfactionProblem for SequencingWithinIntervals {} + +crate::declare_variants! { + default sat SequencingWithinIntervals => "2^num_tasks", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "sequencing_within_intervals", + build: || { + use crate::solvers::BruteForce; + // Instance from the PARTITION reduction example (GJ Theorem 3.8) + let problem = SequencingWithinIntervals::new( + vec![0, 0, 0, 0, 5], + vec![11, 11, 11, 11, 6], + vec![3, 1, 2, 4, 1], + ); + let sample = BruteForce::new() + .find_all_satisfying(&problem) + .into_iter() + .next() + .expect("sequencing_within_intervals example should solve"); + crate::example_db::specs::satisfaction_example(problem, vec![sample]) + }, + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/sequencing_within_intervals.rs"] +mod tests; diff --git a/src/models/mod.rs b/src/models/mod.rs index 5469c3a06..7647e04a4 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -22,6 +22,7 @@ pub use graph::{ }; pub use misc::{ BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, - MinimumTardinessSequencing, PaintShop, ShortestCommonSupersequence, SubsetSum, + MinimumTardinessSequencing, PaintShop, SequencingWithinIntervals, ShortestCommonSupersequence, + SubsetSum, }; pub use set::{ExactCoverBy3Sets, MaximumSetPacking, MinimumSetCovering, SetBasis}; diff --git a/src/unit_tests/models/misc/sequencing_within_intervals.rs b/src/unit_tests/models/misc/sequencing_within_intervals.rs new file mode 100644 index 000000000..b3b5d3205 --- /dev/null +++ b/src/unit_tests/models/misc/sequencing_within_intervals.rs @@ -0,0 +1,155 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::traits::Problem; + +#[test] +fn test_sequencing_within_intervals_creation() { + let problem = SequencingWithinIntervals::new( + vec![0, 0, 0, 0, 5], + vec![11, 11, 11, 11, 6], + vec![3, 1, 2, 4, 1], + ); + assert_eq!(problem.num_tasks(), 5); + assert_eq!(problem.release_times(), &[0, 0, 0, 0, 5]); + assert_eq!(problem.deadlines(), &[11, 11, 11, 11, 6]); + assert_eq!(problem.lengths(), &[3, 1, 2, 4, 1]); + // dims: d[i] - r[i] - l[i] + 1 + // Task 0: 11 - 0 - 3 + 1 = 9 + // Task 1: 11 - 0 - 1 + 1 = 11 + // Task 2: 11 - 0 - 2 + 1 = 10 + // Task 3: 11 - 0 - 4 + 1 = 8 + // Task 4: 6 - 5 - 1 + 1 = 1 + assert_eq!(problem.dims(), vec![9, 11, 10, 8, 1]); +} + +#[test] +fn test_sequencing_within_intervals_evaluation_feasible() { + let problem = SequencingWithinIntervals::new( + vec![0, 0, 0, 0, 5], + vec![11, 11, 11, 11, 6], + vec![3, 1, 2, 4, 1], + ); + // Task 0: config=0 -> start=0, runs [0,3) + // Task 1: config=6 -> start=6, runs [6,7) + // Task 2: config=3 -> start=3, runs [3,5) + // Task 3: config=7 -> start=7, runs [7,11) + // Task 4: config=0 -> start=5, runs [5,6) + // No overlaps. + assert!(problem.evaluate(&[0, 6, 3, 7, 0])); +} + +#[test] +fn test_sequencing_within_intervals_evaluation_infeasible_overlap() { + let problem = SequencingWithinIntervals::new( + vec![0, 0, 0, 0, 5], + vec![11, 11, 11, 11, 6], + vec![3, 1, 2, 4, 1], + ); + // Task 0: config=0 -> start=0, runs [0,3) + // Task 1: config=1 -> start=1, runs [1,2) -- overlaps with task 0 + assert!(!problem.evaluate(&[0, 1, 3, 7, 0])); +} + +#[test] +fn test_sequencing_within_intervals_evaluation_wrong_length() { + let problem = SequencingWithinIntervals::new(vec![0, 2], vec![3, 5], vec![2, 2]); + assert!(!problem.evaluate(&[0])); + assert!(!problem.evaluate(&[0, 0, 0])); +} + +#[test] +fn test_sequencing_within_intervals_evaluation_out_of_range() { + let problem = SequencingWithinIntervals::new(vec![0, 2], vec![3, 5], vec![2, 2]); + // Task 0: dims = 3 - 0 - 2 + 1 = 2, so config must be 0 or 1 + // Task 1: dims = 5 - 2 - 2 + 1 = 2, so config must be 0 or 1 + assert!(!problem.evaluate(&[2, 0])); // out of range for task 0 +} + +#[test] +fn test_sequencing_within_intervals_solver() { + // Simple instance: 3 tasks that can be scheduled sequentially + let problem = SequencingWithinIntervals::new(vec![0, 2, 4], vec![3, 5, 7], vec![2, 2, 2]); + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!(solution.is_some()); + let config = solution.unwrap(); + assert!(problem.evaluate(&config)); +} + +#[test] +fn test_sequencing_within_intervals_solver_partition_example() { + // Instance from the plan (PARTITION reduction) + let problem = SequencingWithinIntervals::new( + vec![0, 0, 0, 0, 5], + vec![11, 11, 11, 11, 6], + vec![3, 1, 2, 4, 1], + ); + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!(solution.is_some()); + let config = solution.unwrap(); + assert!(problem.evaluate(&config)); +} + +#[test] +fn test_sequencing_within_intervals_no_solution() { + // Two tasks that must both use time [0,2), impossible without overlap + let problem = SequencingWithinIntervals::new(vec![0, 0], vec![2, 2], vec![2, 2]); + // Each task has dims = 2 - 0 - 2 + 1 = 1, so config can only be [0, 0] + // Task 0: start=0, runs [0,2) + // Task 1: start=0, runs [0,2) -> overlap + assert!(!problem.evaluate(&[0, 0])); + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!(solution.is_none()); +} + +#[test] +fn test_sequencing_within_intervals_serialization() { + let problem = SequencingWithinIntervals::new(vec![0, 2, 4], vec![3, 5, 7], vec![2, 2, 2]); + let json = serde_json::to_value(&problem).unwrap(); + let restored: SequencingWithinIntervals = serde_json::from_value(json).unwrap(); + assert_eq!(restored.release_times(), problem.release_times()); + assert_eq!(restored.deadlines(), problem.deadlines()); + assert_eq!(restored.lengths(), problem.lengths()); +} + +#[test] +fn test_sequencing_within_intervals_empty() { + let problem = SequencingWithinIntervals::new(vec![], vec![], vec![]); + assert_eq!(problem.num_tasks(), 0); + assert_eq!(problem.dims(), Vec::::new()); + assert!(problem.evaluate(&[])); +} + +#[test] +fn test_sequencing_within_intervals_problem_name() { + assert_eq!( + ::NAME, + "SequencingWithinIntervals" + ); +} + +#[test] +fn test_sequencing_within_intervals_variant() { + let v = ::variant(); + assert!(v.is_empty()); +} + +#[test] +fn test_sequencing_within_intervals_single_task() { + let problem = SequencingWithinIntervals::new(vec![0], vec![5], vec![3]); + // dims = 5 - 0 - 3 + 1 = 3 + assert_eq!(problem.dims(), vec![3]); + // Any valid config should be feasible (only one task, no overlaps possible) + assert!(problem.evaluate(&[0])); + assert!(problem.evaluate(&[1])); + assert!(problem.evaluate(&[2])); +} + +#[test] +#[should_panic(expected = "time window is empty")] +fn test_sequencing_within_intervals_invalid_window() { + // r + l > d: impossible task + SequencingWithinIntervals::new(vec![5], vec![3], vec![2]); +}