Skip to content

Commit 70cd9ae

Browse files
committed
fix: align ILP models and solver pipelines
1 parent 9cc3c1c commit 70cd9ae

175 files changed

Lines changed: 2569 additions & 2716 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/paper/reductions.typ

Lines changed: 66 additions & 144 deletions
Large diffs are not rendered by default.

docs/paper/references.bib

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1796,33 +1796,6 @@ @article{yannakakis1980
17961796
doi = {10.1137/0138030}
17971797
}
17981798

1799-
@inproceedings{edmonds1970,
1800-
author = {Jack Edmonds},
1801-
title = {Submodular functions, matroids, and certain polyhedra},
1802-
booktitle = {Combinatorial Structures and Their Applications},
1803-
pages = {69--87},
1804-
year = {1970},
1805-
publisher = {Gordon and Breach}
1806-
}
1807-
1808-
@article{doron2024,
1809-
author = {Ilan Doron-Arad and Ariel Kulik and Hadas Shachnai},
1810-
title = {You (Almost) Can't Beat Brute Force for 3-Matroid Intersection},
1811-
journal = {arXiv preprint arXiv:2412.02217},
1812-
year = {2024}
1813-
}
1814-
1815-
@article{fomin2019,
1816-
author = {Fedor V. Fomin and Daniel Lokshtanov and Fahad Panolan and Saket Saurabh},
1817-
title = {Exact Algorithms via Monotone Local Search},
1818-
journal = {Journal of the ACM},
1819-
volume = {66},
1820-
number = {2},
1821-
pages = {1--23},
1822-
year = {2019},
1823-
doi = {10.1145/3277568}
1824-
}
1825-
18261799
@book{vonNeumannMorgenstern1944,
18271800
author = {John von Neumann and Oskar Morgenstern},
18281801
title = {Theory of Games and Economic Behavior},

docs/src/design.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,7 @@ proved infeasibility, and `Err` reports an operational failure.
443443
| Solver | Description |
444444
|--------|-------------|
445445
| **BruteForce** | Enumerates a registered finite search space and returns an optimal or satisfying solution. Used for testing and verification. |
446-
| **ILPSolver** | Solves `ILP<bool>` and `ILP<i64>` instances directly with HiGHS via `good_lp`. Also provides `solve_reduced::<V, _>()` for problems that implement `ReduceTo<ILP<V>>`. |
446+
| **ILPSolver** | Executes a problem's registered ILP pipeline. Each pipeline terminates at `ILP<bool, f64>` or `ILP<i64, f64>`, which is solved by HiGHS via `good_lp`. |
447447

448448
## JSON Serialization
449449

docs/src/getting-started.md

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -96,20 +96,20 @@ assert!(metric.is_valid());
9696
Packing solution: [1, 0, 1, 1] -> size Max(3)
9797
```
9898

99-
For convenience, `ILPSolver::solve_reduced` combines reduce + solve + extract
100-
in a single call:
99+
`ILPSolver::solve` executes the problem's registered ILP pipeline, including
100+
all reductions and reverse witness extraction:
101101

102102
```rust,ignore
103103
let solution = ILPSolver::new()
104-
.solve_reduced::<bool, _>(&problem)
104+
.solve(&problem)
105105
.unwrap();
106106
assert!(problem.evaluate(&solution).is_valid());
107107
```
108108

109-
The ILP domain is explicit because a source type may provide more than one
110-
direct ILP reduction. Both `bool` and `i64` are supported. `solve` and
111-
`solve_reduced` return `ILPSolveError`, which distinguishes infeasibility,
112-
timeout, unboundedness, unsupported dynamic input, and backend failure.
109+
The registered path determines the ILP variable domain. Every path ends at an
110+
`ILP<V, f64>` terminal accepted by the HiGHS backend. `solve` returns
111+
`ILPSolveError`, which distinguishes infeasibility, timeout, unboundedness,
112+
missing pipelines, unsupported dynamic input, and backend failure.
113113

114114
### Example 2: Reduction path search — integer factoring to spin glass
115115

@@ -145,10 +145,9 @@ returning the canonical pair **2 × 3**.
145145

146146
#### Step 3 — Solve with ILPSolver
147147

148-
`solve_reduced` reduces the problem to ILP internally and solves it in one
149-
call. It returns a configuration vector for the original problem — no manual
150-
extraction needed. For small instances you can also use `BruteForce`, but
151-
`ILPSolver` scales to much larger problems.
148+
`solve` executes the registered ILP pipeline and returns a configuration for
149+
the original problem — no manual extraction needed. For small instances you
150+
can also use `BruteForce`, but `ILPSolver` scales to much larger problems.
152151

153152
```rust,ignore
154153
{{#include ../../examples/chained_reduction_factoring_to_spinglass.rs:step3}}

problemreductions-cli/src/dispatch.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,7 @@ mod tests {
563563
fn solve_result_json_preserves_structured_solver_contract() {
564564
let result = SolveResult {
565565
solver: problemreductions::solvers::SolverExecution::Ilp {
566-
reduction_path: vec!["Source".to_string(), "ILP<bool>".to_string()],
566+
reduction_path: vec!["Source".to_string(), "ILP<i64, bool>".to_string()],
567567
},
568568
outcome: SolveOutcome::Optimal {
569569
solution: serde_json::json!([true, false]),
@@ -577,7 +577,7 @@ mod tests {
577577
assert_eq!(json["status"], "optimal");
578578
assert_eq!(
579579
json["solver"]["reduction_path"],
580-
serde_json::json!(["Source", "ILP<bool>"])
580+
serde_json::json!(["Source", "ILP<i64, bool>"])
581581
);
582582
assert_eq!(json["solution"], serde_json::json!([true, false]));
583583
assert!(json.get("reduced_to").is_none());

problemreductions-cli/src/problem_name.rs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,6 @@ pub fn resolve_alias(input: &str) -> String {
3939
if input.eq_ignore_ascii_case("MinimumCodeGenerationParallelAssignments") {
4040
return "MinimumCodeGenerationParallelAssignments".to_string();
4141
}
42-
if input.eq_ignore_ascii_case("ThreeMatroidIntersection") {
43-
return "ThreeMatroidIntersection".to_string();
44-
}
4542
if let Some((entry, _)) = problemreductions::registry::find_variant_by_alias(input) {
4643
return entry.name.to_string();
4744
}
@@ -140,6 +137,23 @@ fn resolve_variant_updates(
140137
let mut updated_dimensions = BTreeSet::new();
141138

142139
for token in &spec.variant_values {
140+
if let Some((dimension, value)) = token.split_once('=') {
141+
let values = token_index.get(dimension).ok_or_else(|| {
142+
anyhow::anyhow!(
143+
"Unknown variant dimension \"{dimension}\" for {}",
144+
spec.name
145+
)
146+
})?;
147+
if !values.contains(value) {
148+
anyhow::bail!("Unknown value \"{value}\" for variant dimension \"{dimension}\"");
149+
}
150+
if !updated_dimensions.insert(dimension.to_string()) {
151+
anyhow::bail!("Variant dimension \"{dimension}\" was specified more than once");
152+
}
153+
resolved.insert(dimension.to_string(), value.to_string());
154+
continue;
155+
}
156+
143157
let matching_dimensions = token_index
144158
.iter()
145159
.filter(|(_, values)| values.contains(token))
@@ -368,6 +382,14 @@ mod tests {
368382
assert_eq!(spec.variant_values, vec!["SimpleGraph", "f64"]);
369383
}
370384

385+
#[test]
386+
fn resolve_problem_ref_accepts_named_variant_dimension() {
387+
let graph = problemreductions::rules::ReductionGraph::new();
388+
let resolved = resolve_problem_ref("ILP/variable=i64", &graph).unwrap();
389+
assert_eq!(resolved.variant["variable"], "i64");
390+
assert_eq!(resolved.variant["coefficient"], "i64");
391+
}
392+
371393
#[test]
372394
fn test_resolve_alias_pass_through_undirected_two_commodity_integral_flow() {
373395
assert_eq!(

problemreductions-cli/src/test_support.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ impl AggregateReductionResult for AggregateValueToIlpReduction {
127127
&self.target
128128
}
129129

130-
fn extract_value(&self, _target_value: Extremum<f64>) -> Max<i64> {
130+
fn extract_value(&self, _target_value: Extremum<i64>) -> Max<i64> {
131131
Max(Some(0))
132132
}
133133
}

problemreductions-cli/tests/cli_tests.rs

Lines changed: 86 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3527,7 +3527,7 @@ fn test_solve_direct_ilp_i64_problem() {
35273527
"--example",
35283528
"SequencingToMinimizeWeightedCompletionTime",
35293529
"--to",
3530-
"ILP/i64",
3530+
"ILP/variable=i64",
35313531
"--example-side",
35323532
"target",
35333533
])
@@ -5013,6 +5013,60 @@ fn test_create_length_bounded_disjoint_paths_succeeds() {
50135013
assert_eq!(json["data"]["max_length"], 2);
50145014
}
50155015

5016+
#[test]
5017+
fn test_solve_length_bounded_disjoint_paths_with_chord() {
5018+
use std::io::Write;
5019+
use std::process::Stdio;
5020+
5021+
let created = pred()
5022+
.args([
5023+
"create",
5024+
"LengthBoundedDisjointPaths",
5025+
"--graph",
5026+
"0-1,1-2,0-2",
5027+
"--source",
5028+
"0",
5029+
"--sink",
5030+
"2",
5031+
"--max-length",
5032+
"2",
5033+
])
5034+
.output()
5035+
.unwrap();
5036+
assert!(
5037+
created.status.success(),
5038+
"{}",
5039+
String::from_utf8_lossy(&created.stderr)
5040+
);
5041+
let mut child = pred()
5042+
.args(["solve", "-", "--json", "--quiet"])
5043+
.stdin(Stdio::piped())
5044+
.stdout(Stdio::piped())
5045+
.stderr(Stdio::piped())
5046+
.spawn()
5047+
.unwrap();
5048+
child
5049+
.stdin
5050+
.take()
5051+
.unwrap()
5052+
.write_all(&created.stdout)
5053+
.unwrap();
5054+
let solved = child.wait_with_output().unwrap();
5055+
assert!(
5056+
solved.status.success(),
5057+
"{}",
5058+
String::from_utf8_lossy(&solved.stderr)
5059+
);
5060+
let json: serde_json::Value = serde_json::from_slice(&solved.stdout).unwrap();
5061+
assert_eq!(json["status"], "optimal");
5062+
assert_eq!(json["solver"]["kind"], "ilp");
5063+
assert_eq!(json["evaluation"], "Max(2)");
5064+
let paths = json["solution"].as_array().unwrap();
5065+
assert_eq!(paths.len(), 2);
5066+
assert!(paths.contains(&serde_json::json!([true, true, false])));
5067+
assert!(paths.contains(&serde_json::json!([false, false, true])));
5068+
}
5069+
50165070
#[test]
50175071
fn test_create_length_bounded_disjoint_paths_rejects_negative_bound_value() {
50185072
let output = pred()
@@ -9058,7 +9112,7 @@ fn test_create_minimum_multiway_cut_rejects_single_terminal() {
90589112
}
90599113

90609114
#[test]
9061-
fn test_create_sequencing_within_intervals_rejects_empty_window() {
9115+
fn test_create_and_solve_sequencing_within_intervals_empty_window() {
90629116
let output = pred()
90639117
.args([
90649118
"create",
@@ -9072,16 +9126,35 @@ fn test_create_sequencing_within_intervals_rejects_empty_window() {
90729126
])
90739127
.output()
90749128
.unwrap();
9075-
assert!(!output.status.success());
9076-
let stderr = String::from_utf8_lossy(&output.stderr);
90779129
assert!(
9078-
!stderr.contains("panicked at"),
9079-
"expected graceful CLI error, got panic: {stderr}"
9080-
);
9081-
assert!(
9082-
stderr.contains("task 0 has an empty time window"),
9083-
"expected empty-window validation error, got: {stderr}"
9130+
output.status.success(),
9131+
"{}",
9132+
String::from_utf8_lossy(&output.stderr)
90849133
);
9134+
for solver in ["brute-force", "ilp"] {
9135+
use std::io::Write;
9136+
let mut child = pred()
9137+
.args(["solve", "-", "--solver", solver, "--json"])
9138+
.stdin(std::process::Stdio::piped())
9139+
.stdout(std::process::Stdio::piped())
9140+
.stderr(std::process::Stdio::piped())
9141+
.spawn()
9142+
.unwrap();
9143+
child
9144+
.stdin
9145+
.take()
9146+
.unwrap()
9147+
.write_all(&output.stdout)
9148+
.unwrap();
9149+
let solved = child.wait_with_output().unwrap();
9150+
assert!(
9151+
solved.status.success(),
9152+
"{}",
9153+
String::from_utf8_lossy(&solved.stderr)
9154+
);
9155+
let json: serde_json::Value = serde_json::from_slice(&solved.stdout).unwrap();
9156+
assert_eq!(json["status"], "infeasible");
9157+
}
90859158
}
90869159

90879160
#[test]
@@ -9118,11 +9191,11 @@ fn test_create_sequencing_within_intervals_rejects_overflow() {
91189191
"create",
91199192
"SequencingWithinIntervals",
91209193
"--release-times",
9121-
"9223372036854775807",
9194+
"0",
91229195
"--deadlines",
91239196
"9223372036854775807",
91249197
"--lengths",
9125-
"1",
9198+
"0",
91269199
])
91279200
.output()
91289201
.unwrap();
@@ -9133,7 +9206,7 @@ fn test_create_sequencing_within_intervals_rejects_overflow() {
91339206
"expected graceful CLI error, got panic: {stderr}"
91349207
);
91359208
assert!(
9136-
stderr.contains("task 0 release time plus length overflows i64"),
9209+
stderr.contains("task start-slot count overflows i64"),
91379210
"expected overflow validation error, got: {stderr}"
91389211
);
91399212
}

src/example_db/specs.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,30 @@ where
7575
<S as ReduceTo<crate::models::algebraic::ILP<V>>>::Result:
7676
ReductionResult<Source = S, Target = crate::models::algebraic::ILP<V>>,
7777
S::Solution: Serialize,
78+
{
79+
rule_example_via_typed_ilp::<S, V, i64>(source)
80+
}
81+
82+
/// Float-coefficient counterpart of [`rule_example_via_ilp`].
83+
pub fn rule_example_via_float_ilp<S, V>(source: S) -> RuleExample
84+
where
85+
S: Problem + Serialize + ReduceTo<crate::models::algebraic::ILP<V, f64>>,
86+
V: crate::models::algebraic::VariableDomain,
87+
<S as ReduceTo<crate::models::algebraic::ILP<V, f64>>>::Result:
88+
ReductionResult<Source = S, Target = crate::models::algebraic::ILP<V, f64>>,
89+
S::Solution: Serialize,
90+
{
91+
rule_example_via_typed_ilp::<S, V, f64>(source)
92+
}
93+
94+
fn rule_example_via_typed_ilp<S, V, C>(source: S) -> RuleExample
95+
where
96+
S: Problem + Serialize + ReduceTo<crate::models::algebraic::ILP<V, C>>,
97+
V: crate::models::algebraic::VariableDomain,
98+
C: crate::models::algebraic::ILPCoefficient + Serialize,
99+
<S as ReduceTo<crate::models::algebraic::ILP<V, C>>>::Result:
100+
ReductionResult<Source = S, Target = crate::models::algebraic::ILP<V, C>>,
101+
S::Solution: Serialize,
78102
{
79103
use crate::export::SolutionPair;
80104
let reduction = source.reduce_to().expect("reduction should succeed");

src/lib.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,6 @@ pub mod prelude {
9797
ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, IntegerKnapsack,
9898
MaximumSetPacking, MinimumCardinalityKey, MinimumHittingSet, MinimumSetCovering,
9999
PrimeAttributeName, RootedTreeStorageAssignment, SetBasis, SetSplitting,
100-
ThreeMatroidIntersection,
101100
};
102101

103102
// Core traits

0 commit comments

Comments
 (0)