Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
70 changes: 70 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
"SubgraphIsomorphism": [Subgraph Isomorphism],
"PartitionIntoTriangles": [Partition Into Triangles],
"FlowShopScheduling": [Flow Shop Scheduling],
"MultiprocessorScheduling": [Multiprocessor Scheduling],
"MinimumTardinessSequencing": [Minimum Tardiness Sequencing],
"SequencingWithinIntervals": [Sequencing Within Intervals],
"DirectedTwoCommodityIntegralFlow": [Directed Two-Commodity Integral Flow],
Expand Down Expand Up @@ -2377,6 +2378,75 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS
) <fig:flowshop>
]

#{
let x = load-model-example("MultiprocessorScheduling")
let lengths = x.instance.lengths
let num-processors = x.instance.num_processors
let deadline = x.instance.deadline
let assignment = x.optimal.at(0).config
let tasks-by-processor = range(num-processors).map(p =>
range(lengths.len()).filter(i => assignment.at(i) == p)
)
let loads = tasks-by-processor.map(tasks => tasks.map(i => lengths.at(i)).sum())
let max-x = (num-processors - 1) * 1.8 + 1.0
[
#problem-def("MultiprocessorScheduling")[
Given a finite set $T$ of tasks with processing lengths $ell: T -> ZZ^+$, a number $m in ZZ^+$ of identical processors, and a deadline $D in ZZ^+$, determine whether there exists an assignment $p: T -> {1, dots, m}$ such that for every processor $i in {1, dots, m}$ we have $sum_(t in T: p(t) = i) ell(t) <= D$.
][
Multiprocessor Scheduling is problem SS8 in Garey & Johnson @garey1979. Their original formulation uses start times on identical processors, but because tasks are independent and non-preemptive, any feasible schedule can be packed contiguously on each processor. The model implemented here therefore uses processor-assignment variables, and feasibility reduces to checking that every processor's total load is at most $D$. For fixed $m$, dynamic programming over load vectors gives pseudo-polynomial algorithms; for general $m$, the best known exact algorithm runs in $O^*(2^n)$ time via inclusion-exclusion over set partitions @bjorklund2009.

*Example.* Let $T = {t_1, dots, t_5}$ with lengths $(4, 5, 3, 2, 6)$, $m = 2$, and $D = 10$. The satisfying assignment $(1, 2, 2, 2, 1)$ places $t_1$ and $t_5$ on processor 1 and $t_2, t_3, t_4$ on processor 2. The verifier computes the processor loads $4 + 6 = 10$ and $5 + 3 + 2 = 10$, so both meet the deadline exactly.

#figure({
canvas(length: 1cm, {
let scale = 0.25
let width = 1.0
let gap = 0.8
let colors = (
rgb("#4e79a7"),
rgb("#e15759"),
rgb("#76b7b2"),
rgb("#f28e2b"),
rgb("#59a14f"),
)

for p in range(num-processors) {
let x0 = p * (width + gap)
draw.rect((x0, 0), (x0 + width, deadline * scale), stroke: 0.8pt + black)
let y = 0
for task in tasks-by-processor.at(p) {
let len = lengths.at(task)
let col = colors.at(task)
draw.rect(
(x0, y),
(x0 + width, y + len * scale),
fill: col.transparentize(25%),
stroke: 0.4pt + col,
)
draw.content(
(x0 + width / 2, y + len * scale / 2),
text(7pt, fill: white)[$t_#(task + 1)$],
)
y += len * scale
}
draw.content((x0 + width / 2, -0.3), text(8pt)[$P_#(p + 1)$])
draw.content((x0 + width / 2, deadline * scale + 0.25), text(7pt)[$L_#(p + 1) = #loads.at(p)$])
}

draw.line(
(-0.15, deadline * scale),
(max-x + 0.15, deadline * scale),
stroke: (dash: "dashed", paint: luma(150), thickness: 0.5pt),
)
draw.content((-0.45, deadline * scale), text(7pt)[$D$])
})
},
caption: [Canonical Multiprocessor Scheduling instance with 5 tasks on 2 processors. Stacked blocks show the satisfying assignment $(1, 2, 2, 2, 1)$; both processor loads equal the deadline $D = 10$.],
) <fig:multiprocessor-scheduling>
]
]
}

#{
let x = load-model-example("SequencingWithinIntervals")
let ntasks = x.instance.lengths.len()
Expand Down
10 changes: 10 additions & 0 deletions docs/paper/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ @article{shang2018
doi = {10.1016/j.cor.2017.10.015}
}

@article{brucker1977,
author = {Peter Brucker and Jan Karel Lenstra and Alexander H. G. Rinnooy Kan},
title = {Complexity of Machine Scheduling Problems},
journal = {Annals of Discrete Mathematics},
volume = {1},
pages = {343--362},
year = {1977},
doi = {10.1016/S0167-5060(08)70743-X}
}

@inproceedings{karp1972,
author = {Richard M. Karp},
title = {Reducibility among Combinatorial Problems},
Expand Down
5 changes: 3 additions & 2 deletions docs/src/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -515,8 +515,9 @@ Source evaluation: Valid(2)
```

> **Note:** The ILP solver requires a reduction path from the target problem to ILP.
> Some problems (e.g., BoundedComponentSpanningForest, LengthBoundedDisjointPaths) do not currently have one, so use
> `pred solve <file> --solver brute-force` for these.
> Some problems do not currently have one. Examples include BoundedComponentSpanningForest,
> LengthBoundedDisjointPaths, QUBO, SpinGlass, MaxCut, CircuitSAT, and MultiprocessorScheduling.
> Use `pred solve <file> --solver brute-force` for these, or reduce to a problem that supports ILP first.
> For other problems, use `pred path <PROBLEM> ILP` to check whether an ILP reduction path exists.

## Shell Completions
Expand Down
8 changes: 5 additions & 3 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ Flags by problem type:
BMF --matrix (0/1), --rank
SteinerTree --graph, --edge-weights, --terminals
CVP --basis, --target-vec [--bounds]
MultiprocessorScheduling --lengths, --num-processors, --deadline
SequencingWithinIntervals --release-times, --deadlines, --lengths
OptimalLinearArrangement --graph, --bound
RuralPostman (RPP) --graph, --edge-weights, --required-edges, --bound
Expand Down Expand Up @@ -276,6 +277,7 @@ Examples:
pred create MIS/KingsSubgraph --positions \"0,0;1,0;1,1;0,1\"
pred create MIS/UnitDiskGraph --positions \"0,0;1,0;0.5,0.8\" --radius 1.5
pred create MIS --random --num-vertices 10 --edge-prob 0.3
pred create MultiprocessorScheduling --lengths 4,5,3,2,6 --num-processors 2 --deadline 10
pred create BiconnectivityAugmentation --graph 0-1,1-2,2-3 --potential-edges 0-2:3,0-3:4,1-3:2 --budget 5
pred create FVS --arcs \"0>1,1>2,2>0\" --weights 1,1,1
pred create UndirectedTwoCommodityIntegralFlow --graph 0-2,1-2,2-3 --capacities 1,1,2 --source-1 0 --sink-1 3 --source-2 1 --sink-2 3 --requirement-1 1 --requirement-2 1
Expand Down Expand Up @@ -432,7 +434,7 @@ pub struct CreateArgs {
/// 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")
/// Processing lengths (comma-separated, e.g., "4,5,3,2,6")
#[arg(long)]
pub lengths: Option<String>,
/// Terminal vertices for SteinerTree or MinimumMultiwayCut (comma-separated indices, e.g., "0,2,4")
Expand Down Expand Up @@ -474,10 +476,10 @@ pub struct CreateArgs {
/// Task lengths for FlowShopScheduling (semicolon-separated rows: "3,4,2;2,3,5;4,1,3")
#[arg(long)]
pub task_lengths: Option<String>,
/// Deadline for FlowShopScheduling
/// Deadline for FlowShopScheduling or MultiprocessorScheduling
#[arg(long)]
pub deadline: Option<u64>,
/// Number of processors/machines for FlowShopScheduling
/// Number of processors/machines for FlowShopScheduling or MultiprocessorScheduling
#[arg(long)]
pub num_processors: Option<usize>,
/// Alphabet size for SCS (optional; inferred from max symbol + 1 if omitted)
Expand Down
35 changes: 32 additions & 3 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ use problemreductions::models::graph::{
};
use problemreductions::models::misc::{
BinPacking, FlowShopScheduling, LongestCommonSubsequence, MinimumTardinessSequencing,
PaintShop, SequencingWithinIntervals, ShortestCommonSupersequence, SubsetSum,
MultiprocessorScheduling, PaintShop, SequencingWithinIntervals, ShortestCommonSupersequence,
SubsetSum,
};
use problemreductions::models::BiconnectivityAugmentation;
use problemreductions::prelude::*;
Expand Down Expand Up @@ -226,7 +227,7 @@ fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str {
Some("UnitDiskGraph") => "float positions: \"0.0,0.0;1.0,0.0\"",
_ => "edge list: 0-1,1-2,2-3",
},
"Vec<u64>" => "comma-separated integers: 1,2,3",
"Vec<u64>" => "comma-separated integers: 4,5,3,2,6",
"Vec<W>" => "comma-separated: 1,2,3",
"Vec<usize>" => "comma-separated indices: 0,2,4",
"Vec<(usize, usize, W)>" | "Vec<(usize,usize,W)>" => {
Expand Down Expand Up @@ -288,6 +289,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",
"MultiprocessorScheduling" => "--lengths 4,5,3,2,6 --num-processors 2 --deadline 10",
"MinimumMultiwayCut" => "--graph 0-1,1-2,2-3 --terminals 0,2 --edge-weights 1,1,1",
"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",
Expand Down Expand Up @@ -1286,6 +1288,34 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
)
}

// MultiprocessorScheduling
"MultiprocessorScheduling" => {
let usage = "Usage: pred create MultiprocessorScheduling --lengths 4,5,3,2,6 --num-processors 2 --deadline 10";
let lengths_str = args.lengths.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"MultiprocessorScheduling requires --lengths, --num-processors, and --deadline\n\n{usage}"
)
})?;
let num_processors = args.num_processors.ok_or_else(|| {
anyhow::anyhow!("MultiprocessorScheduling requires --num-processors\n\n{usage}")
})?;
if num_processors == 0 {
bail!("MultiprocessorScheduling requires --num-processors > 0\n\n{usage}");
}
let deadline = args.deadline.ok_or_else(|| {
anyhow::anyhow!("MultiprocessorScheduling requires --deadline\n\n{usage}")
})?;
let lengths: Vec<u64> = util::parse_comma_list(lengths_str)?;
(
ser(MultiprocessorScheduling::new(
lengths,
num_processors,
deadline,
))?,
resolved_variant.clone(),
)
}

// MinimumMultiwayCut
"MinimumMultiwayCut" => {
let (graph, _) = parse_graph(args).map_err(|e| {
Expand Down Expand Up @@ -1708,7 +1738,6 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
resolved_variant.clone(),
)
}

_ => bail!("{}", crate::problem_name::unknown_problem_error(canonical)),
};

Expand Down
15 changes: 12 additions & 3 deletions problemreductions-cli/src/commands/inspect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,17 @@ fn inspect_problem(pj: &ProblemJson, out: &OutputConfig) -> Result<()> {
}
text.push_str(&format!("Variables: {}\n", problem.num_variables_dyn()));

// Solvers
text.push_str("Solvers: ilp (default), brute-force\n");
let solvers = if problem.supports_ilp_solver() {
vec!["ilp", "brute-force"]
} else {
vec!["brute-force"]
};
let solver_summary = if solvers.first() == Some(&"ilp") {
"ilp (default), brute-force".to_string()
} else {
"brute-force".to_string()
};
text.push_str(&format!("Solvers: {solver_summary}\n"));

// Reductions
let outgoing = graph.outgoing_reductions(name);
Expand All @@ -56,7 +65,7 @@ fn inspect_problem(pj: &ProblemJson, out: &OutputConfig) -> Result<()> {
"variant": variant,
"size_fields": size_fields,
"num_variables": problem.num_variables_dyn(),
"solvers": ["ilp", "brute-force"],
"solvers": solvers,
"reduces_to": targets,
});

Expand Down
15 changes: 13 additions & 2 deletions problemreductions-cli/src/commands/solve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ fn solve_problem(
result
}
"ilp" => {
let result = problem.solve_with_ilp()?;
let result = problem.solve_with_ilp().map_err(add_ilp_solver_hint)?;
let solver_desc = if name == "ILP" {
"ilp".to_string()
} else {
Expand Down Expand Up @@ -139,7 +139,7 @@ fn solve_bundle(bundle: ReductionBundle, solver_name: &str, out: &OutputConfig)
// 2. Solve the target problem
let target_result = match solver_name {
"brute-force" => target.solve_brute_force()?,
"ilp" => target.solve_with_ilp()?,
"ilp" => target.solve_with_ilp().map_err(add_ilp_solver_hint)?,
_ => unreachable!(),
};

Expand Down Expand Up @@ -200,3 +200,14 @@ fn solve_bundle(bundle: ReductionBundle, solver_name: &str, out: &OutputConfig)
}
result
}

fn add_ilp_solver_hint(err: anyhow::Error) -> anyhow::Error {
let message = err.to_string();
if message.starts_with("No reduction path from ") && message.ends_with(" to ILP") {
anyhow::anyhow!(
"{message}\n\nHint: try `--solver brute-force` for direct exhaustive search on small instances."
)
} else {
err
}
}
74 changes: 53 additions & 21 deletions problemreductions-cli/src/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,53 +47,63 @@ impl LoadedProblem {
Ok(SolveResult { config, evaluation })
}

pub fn supports_ilp_solver(&self) -> bool {
let name = self.problem_name();
name == "ILP" || self.best_ilp_reduction_path().is_some()
}

/// Solve using the ILP solver. If the problem is not ILP, auto-reduce to ILP first.
pub fn solve_with_ilp(&self) -> Result<SolveResult> {
let name = self.problem_name();
if name == "ILP" {
return solve_ilp(self.as_any());
}

// Auto-reduce to ILP, solve, and map solution back
let reduction_path = self.best_ilp_reduction_path().ok_or_else(|| {
anyhow::anyhow!(
"No reduction path from {} to ILP. Try `--solver brute-force`, or reduce to a problem that supports ILP.",
name
)
})?;
let graph = ReductionGraph::new();

let chain = graph
.reduce_along_path(&reduction_path, self.as_any())
.ok_or_else(|| anyhow::anyhow!("Failed to execute reduction chain to ILP"))?;

let ilp_result = solve_ilp(chain.target_problem_any())?;
let config = chain.extract_solution(&ilp_result.config);
let evaluation = self.evaluate_dyn(&config);
Ok(SolveResult { config, evaluation })
}

fn best_ilp_reduction_path(&self) -> Option<problemreductions::rules::ReductionPath> {
let name = self.problem_name();
let source_variant = self.variant_map();
let graph = ReductionGraph::new();
let ilp_variants = graph.variants_for("ILP");
let input_size = ProblemSize::new(vec![]);

let mut best_path = None;
for dv in &ilp_variants {
if let Some(p) = graph.find_cheapest_path(
if let Some(path) = graph.find_cheapest_path(
name,
&source_variant,
"ILP",
dv,
&input_size,
&MinimizeSteps,
) {
let is_better = best_path
.as_ref()
.is_none_or(|bp: &problemreductions::rules::ReductionPath| p.len() < bp.len());
let is_better = best_path.as_ref().is_none_or(
|current: &problemreductions::rules::ReductionPath| path.len() < current.len(),
);
if is_better {
best_path = Some(p);
best_path = Some(path);
}
}
}

let reduction_path = best_path.ok_or_else(|| {
anyhow::anyhow!(
"No reduction path from {} to ILP. Try `--solver brute-force`, or reduce to a problem that supports ILP.",
name
)
})?;

let chain = graph
.reduce_along_path(&reduction_path, self.as_any())
.ok_or_else(|| anyhow::anyhow!("Failed to execute reduction chain to ILP"))?;

let ilp_result = solve_ilp(chain.target_problem_any())?;
let config = chain.extract_solution(&ilp_result.config);
let evaluation = self.evaluate_dyn(&config);
Ok(SolveResult { config, evaluation })
best_path
}
}

Expand Down Expand Up @@ -249,4 +259,26 @@ mod tests {
let json = serialize_any_problem("BinPacking", &variant, &problem as &dyn Any).unwrap();
assert_eq!(json, serde_json::to_value(&problem).unwrap());
}

#[test]
fn test_load_problem_rejects_zero_processor_multiprocessor_scheduling() {
let loaded = load_problem(
"MultiprocessorScheduling",
&BTreeMap::new(),
serde_json::json!({
"lengths": [1, 2],
"num_processors": 0,
"deadline": 5
}),
);
assert!(
loaded.is_err(),
"zero-processor instance should be rejected"
);
let err = loaded.err().unwrap();
assert!(
err.to_string().contains("expected positive integer, got 0"),
"unexpected error: {err}"
);
}
}
Loading
Loading