Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
9034263
Add plan for #212: [Model] MultiprocessorScheduling
zazabap Mar 10, 2026
aa7e216
Add plan for #219: [Model] SequencingWithinIntervals
zazabap Mar 10, 2026
339db84
feat: implement SequencingWithinIntervals model for #219
zazabap Mar 10, 2026
7b250c2
resolve merge conflicts with main
zazabap Mar 15, 2026
7dbcf40
fix: update SequencingWithinIntervals for new API
zazabap Mar 15, 2026
255520a
fix: address Copilot review comments
zazabap Mar 15, 2026
ba26385
fix: add missing structural items for SequencingWithinIntervals
zazabap Mar 15, 2026
1000567
chore: regenerate JSON artifacts for SequencingWithinIntervals
zazabap Mar 15, 2026
c6cc3d4
style: consolidate SequencingWithinIntervals re-exports into existing…
zazabap Mar 15, 2026
c0e141b
chore: trigger CI
zazabap Mar 15, 2026
b0d3ed7
style: apply rustfmt
zazabap Mar 15, 2026
c837ab4
fix: remove unreachable deadline check in evaluate()
zazabap Mar 16, 2026
6154a5f
Merge main: resolve conflicts with MinimumTardinessSequencing
zazabap Mar 16, 2026
341dc74
Merge main: resolve SteinerTree conflicts
zazabap Mar 16, 2026
1259e8c
fix: align with PR #192 standard — use load-model-example, remove unr…
zazabap Mar 16, 2026
be242aa
fix: add Gantt chart figure, remove plan doc
zazabap Mar 16, 2026
6d97799
fix: add CLI tests, inline dims computation in evaluate()
zazabap Mar 16, 2026
d8adf5b
Merge main: resolve conflicts with SteinerTree, SetBasis, DirectedTwo…
GiggleLiu Mar 16, 2026
6abf953
Merge fork: integrate CLI tests and Gantt chart updates
GiggleLiu Mar 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -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],
)

Expand Down Expand Up @@ -2057,6 +2058,100 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS
) <fig:flowshop>
]

#{
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.],
) <fig:swi>
]
]
}
#{
let x = load-model-example("MinimumTardinessSequencing")
let ntasks = x.instance.num_tasks
Expand Down
7 changes: 7 additions & 0 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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<String>,
/// Release times for SequencingWithinIntervals (comma-separated, e.g., "0,0,5")
#[arg(long)]
pub release_times: Option<String>,
/// Processing lengths for SequencingWithinIntervals (comma-separated, e.g., "3,1,1")
#[arg(long)]
pub lengths: Option<String>,
/// Terminal vertices for SteinerTree (comma-separated indices, e.g., "0,2,4")
#[arg(long)]
pub terminals: Option<String>,
Expand Down
33 changes: 32 additions & 1 deletion problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -227,6 +231,7 @@ fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str {
"Vec<Vec<usize>>" => "semicolon-separated groups: \"0,1;2,3\"",
"usize" => "integer",
"u64" => "integer",
"Vec<u64>" => "comma-separated integers: 0,0,5",
"i64" => "integer",
"BigUint" => "nonnegative decimal integer",
"Vec<BigUint>" => "comma-separated nonnegative decimal integers: 3,7,1,8",
Expand Down Expand Up @@ -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" => {
Expand Down Expand Up @@ -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<u64> = util::parse_comma_list(rt_str)?;
let deadlines: Vec<u64> = util::parse_comma_list(dl_str)?;
let lengths: Vec<u64> = 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| {
Expand Down
73 changes: 73 additions & 0 deletions problemreductions-cli/tests/cli_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
1 change: 1 addition & 0 deletions src/example_db/fixtures/examples.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}}]},
Expand Down
3 changes: 2 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions src/models/misc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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;

Expand All @@ -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;

Expand All @@ -36,6 +39,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::M
let mut specs = Vec::new();
specs.extend(factoring::canonical_model_example_specs());
specs.extend(paintshop::canonical_model_example_specs());
specs.extend(sequencing_within_intervals::canonical_model_example_specs());
specs.extend(shortest_common_supersequence::canonical_model_example_specs());
specs.extend(minimum_tardiness_sequencing::canonical_model_example_specs());
specs
Expand Down
Loading
Loading