Skip to content

Commit 0980d5a

Browse files
isPANNclaudezazabap
authored
Replace hardcoded canonical rule examples with ILPSolver calls (#775)
* Replace hardcoded canonical rule examples with ILPSolver calls Convert 46 ILP reduction rules from hardcoded SolutionPair to dynamic ILPSolver::new().solve() calls, ensuring canonical examples actually verify the ILP solver can solve the reduction. Also adds a z upper bound constraint in MinMaxMulticenter ILP formulation to prevent HiGHS from stalling on unbounded [0, 2^31) domain. Closes #772 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: extract rule_example_via_ilp helper, eliminating 60x boilerplate Add `rule_example_via_ilp<S, V>()` to `example_db::specs` that performs reduce → ILP solve → extract → assemble in one call. This replaces the 8-line block that was copy-pasted across 60 ILP rule files, and also eliminates the double `reduce_to()` call (once explicit, once inside `rule_example_with_witness`). Also fixes stale docstring on `build_example_db()` which incorrectly claimed no solver is called. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR #775 review comments - Fix indentation in ~32 canonical_rule_example_specs functions (cargo fmt) - Fix incorrect overhead in minmaxmulticenter_ilp: num_constraints formula was 2n²+4n+2, correct is 2n²+3n+2 (matching actual constraint count: 1 cardinality + n assignment + n² link + n x-bounds + n² y-bounds + 1 z-bound + n minimax) - Fix Vec::with_capacity hint to match corrected formula - Remove 6 unused `use crate::rules::ReduceTo as _;` imports in files that switched to rule_example_via_ilp Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: zazabap <sweynan@icloud.com>
1 parent 093c595 commit 0980d5a

File tree

64 files changed

+125
-841
lines changed

Some content is hidden

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

64 files changed

+125
-841
lines changed

src/example_db/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ fn validate_model_uniqueness(models: &[ModelExample]) -> Result<()> {
5353

5454
/// Build the full example database from specs.
5555
///
56-
/// Fast — specs store concrete instances and pre-computed solutions,
57-
/// no solver is called.
56+
/// ILP rule examples call the ILP solver at build time to compute solutions
57+
/// dynamically (feature-gated behind `ilp-solver`).
5858
pub fn build_example_db() -> Result<ExampleDb> {
5959
let model_db = build_model_db()?;
6060
let rule_db = build_rule_db()?;

src/example_db/specs.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,32 @@ where
6262
let target = reduction.target_problem();
6363
assemble_rule_example(&source, target, vec![solution])
6464
}
65+
66+
/// Reduce the source to an ILP, solve it, and assemble the rule example.
67+
///
68+
/// This is the standard pattern for canonical ILP rule examples: reduce once,
69+
/// solve the ILP, extract the source config, and build the example — avoiding
70+
/// the double `reduce_to()` that would occur with `rule_example_with_witness`.
71+
#[cfg(feature = "ilp-solver")]
72+
pub fn rule_example_via_ilp<S, V>(source: S) -> RuleExample
73+
where
74+
S: Problem + Serialize + ReduceTo<crate::models::algebraic::ILP<V>>,
75+
V: crate::models::algebraic::VariableDomain,
76+
<S as ReduceTo<crate::models::algebraic::ILP<V>>>::Result:
77+
ReductionResult<Source = S, Target = crate::models::algebraic::ILP<V>>,
78+
{
79+
use crate::export::SolutionPair;
80+
let reduction = source.reduce_to();
81+
let ilp_solution = crate::solvers::ILPSolver::new()
82+
.solve(reduction.target_problem())
83+
.expect("canonical example must be ILP-solvable");
84+
let source_config = reduction.extract_solution(&ilp_solution);
85+
assemble_rule_example(
86+
&source,
87+
reduction.target_problem(),
88+
vec![SolutionPair {
89+
source_config,
90+
target_config: ilp_solution,
91+
}],
92+
)
93+
}

src/rules/bmf_ilp.rs

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -114,34 +114,12 @@ impl ReduceTo<ILP<bool>> for BMF {
114114

115115
#[cfg(feature = "example-db")]
116116
pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
117-
use crate::export::SolutionPair;
118117
vec![crate::example_db::specs::RuleExampleSpec {
119118
id: "bmf_to_ilp",
120119
build: || {
121120
// 2x2 identity matrix, rank 2
122121
let source = BMF::new(vec![vec![true, false], vec![false, true]], 2);
123-
// B = [[1,0],[0,1]], C = [[1,0],[0,1]]
124-
// b: [1,0,0,1], c: [1,0,0,1]
125-
let source_config = vec![1, 0, 0, 1, 1, 0, 0, 1];
126-
let reduction: ReductionBMFToILP = ReduceTo::<ILP<bool>>::reduce_to(&source);
127-
// Build target config by encoding:
128-
// p_{0,0,0}=1, p_{0,0,1}=0, p_{0,1,0}=0, p_{0,1,1}=0
129-
// p_{1,0,0}=0, p_{1,0,1}=0, p_{1,1,0}=0, p_{1,1,1}=1
130-
// w: [1,0,0,1], e: [0,0,0,0]
131-
let target_config = vec![
132-
1, 0, 0, 1, // B
133-
1, 0, 0, 1, // C
134-
1, 0, 0, 0, 0, 0, 0, 1, // P
135-
1, 0, 0, 1, // W
136-
0, 0, 0, 0, // E
137-
];
138-
crate::example_db::specs::rule_example_with_witness::<_, ILP<bool>>(
139-
source,
140-
SolutionPair {
141-
source_config,
142-
target_config,
143-
},
144-
)
122+
crate::example_db::specs::rule_example_via_ilp::<_, bool>(source)
145123
},
146124
}]
147125
}

src/rules/bottlenecktravelingsalesman_ilp.rs

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,6 @@ impl ReduceTo<ILP<i32>> for BottleneckTravelingSalesman {
174174

175175
#[cfg(feature = "example-db")]
176176
pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
177-
use crate::export::SolutionPair;
178177
vec![crate::example_db::specs::RuleExampleSpec {
179178
id: "bottlenecktravelingsalesman_to_ilp",
180179
build: || {
@@ -183,18 +182,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
183182
crate::topology::SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]),
184183
vec![1, 2, 3, 4],
185184
);
186-
let reduction = ReduceTo::<ILP<i32>>::reduce_to(&source);
187-
let ilp_solution = crate::solvers::ILPSolver::new()
188-
.solve(reduction.target_problem())
189-
.expect("canonical example must be solvable");
190-
let source_config = reduction.extract_solution(&ilp_solution);
191-
crate::example_db::specs::rule_example_with_witness::<_, ILP<i32>>(
192-
source,
193-
SolutionPair {
194-
source_config,
195-
target_config: ilp_solution,
196-
},
197-
)
185+
crate::example_db::specs::rule_example_via_ilp::<_, i32>(source)
198186
},
199187
}]
200188
}

src/rules/capacityassignment_ilp.rs

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,6 @@ impl ReduceTo<ILP<bool>> for CapacityAssignment {
101101

102102
#[cfg(feature = "example-db")]
103103
pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
104-
use crate::export::SolutionPair;
105-
106104
vec![crate::example_db::specs::RuleExampleSpec {
107105
id: "capacityassignment_to_ilp",
108106
build: || {
@@ -121,15 +119,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
121119
vec![vec![8, 4], vec![7, 3]],
122120
12,
123121
);
124-
crate::example_db::specs::rule_example_with_witness::<_, ILP<bool>>(
125-
source,
126-
SolutionPair {
127-
// link 0 → cap 1, link 1 → cap 0
128-
source_config: vec![1, 0],
129-
// x_{0,0}=0, x_{0,1}=1, x_{1,0}=1, x_{1,1}=0
130-
target_config: vec![0, 1, 1, 0],
131-
},
132-
)
122+
crate::example_db::specs::rule_example_via_ilp::<_, bool>(source)
133123
},
134124
}]
135125
}

src/rules/consecutiveblockminimization_ilp.rs

Lines changed: 1 addition & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,6 @@ impl ReduceTo<ILP<bool>> for ConsecutiveBlockMinimization {
113113

114114
#[cfg(feature = "example-db")]
115115
pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
116-
use crate::export::SolutionPair;
117116
vec![crate::example_db::specs::RuleExampleSpec {
118117
id: "consecutiveblockminimization_to_ilp",
119118
build: || {
@@ -122,62 +121,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
122121
vec![vec![true, false, true], vec![false, true, true]],
123122
2,
124123
);
125-
// Permutation [2,0,1] => columns reordered as [2,0,1]
126-
// Row 0: A[0,2]=1, A[0,0]=1, A[0,1]=0 => [1,1,0] => 1 block
127-
// Row 1: A[1,2]=1, A[1,0]=0, A[1,1]=1 => [1,0,1] => 2 blocks
128-
// Total = 3 > 2, try [0,2,1]:
129-
// Row 0: A[0,0]=1, A[0,2]=1, A[0,1]=0 => [1,1,0] => 1 block
130-
// Row 1: A[1,0]=0, A[1,2]=1, A[1,1]=1 => [0,1,1] => 1 block
131-
// Total = 2 <= 2. Good.
132-
let source_config = vec![0, 2, 1];
133-
let reduction: ReductionCBMToILP = ReduceTo::<ILP<bool>>::reduce_to(&source);
134-
// Encode x_{c,p}: column c at position p
135-
// c=0 at p=0: x_{0*3+0}=1, c=2 at p=1: x_{2*3+1}=1, c=1 at p=2: x_{1*3+2}=1
136-
let n = 3;
137-
let mut target_config = vec![0; reduction.target.num_vars];
138-
// x_{0,0} = 1
139-
target_config[0 * n + 0] = 1;
140-
// x_{2,1} = 1
141-
target_config[2 * n + 1] = 1;
142-
// x_{1,2} = 1
143-
target_config[1 * n + 2] = 1;
144-
// a values
145-
let a_offset = n * n;
146-
let m = 2;
147-
let matrix = vec![vec![true, false, true], vec![false, true, true]];
148-
let perm = [0, 2, 1];
149-
for r in 0..m {
150-
for p in 0..n {
151-
if matrix[r][perm[p]] {
152-
target_config[a_offset + r * n + p] = 1;
153-
}
154-
}
155-
}
156-
// b values
157-
let b_offset = n * n + m * n;
158-
for r in 0..m {
159-
for p in 0..n {
160-
let a_cur = if matrix[r][perm[p]] { 1 } else { 0 };
161-
let a_prev = if p > 0 && matrix[r][perm[p - 1]] {
162-
1
163-
} else {
164-
0
165-
};
166-
if p == 0 {
167-
target_config[b_offset + r * n + p] = a_cur;
168-
} else if a_cur > a_prev {
169-
target_config[b_offset + r * n + p] = 1;
170-
}
171-
}
172-
}
173-
174-
crate::example_db::specs::rule_example_with_witness::<_, ILP<bool>>(
175-
source,
176-
SolutionPair {
177-
source_config,
178-
target_config,
179-
},
180-
)
124+
crate::example_db::specs::rule_example_via_ilp::<_, bool>(source)
181125
},
182126
}]
183127
}

src/rules/consistencyofdatabasefrequencytables_ilp.rs

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ impl ReductionCDFTToILP {
5555
}
5656

5757
/// Encode a satisfying source assignment as a concrete ILP variable vector.
58-
#[cfg_attr(not(any(test, feature = "example-db")), allow(dead_code))]
58+
#[cfg_attr(not(test), allow(dead_code))]
5959
pub(crate) fn encode_source_solution(&self, source_solution: &[usize]) -> Vec<usize> {
6060
let mut target_solution = vec![0usize; self.target.num_vars];
6161
let num_attributes = self.source.num_attributes();
@@ -204,7 +204,6 @@ impl ReduceTo<ILP<bool>> for ConsistencyOfDatabaseFrequencyTables {
204204

205205
#[cfg(feature = "example-db")]
206206
pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
207-
use crate::export::SolutionPair;
208207
use crate::models::misc::{FrequencyTable, KnownValue};
209208

210209
vec![crate::example_db::specs::RuleExampleSpec {
@@ -223,16 +222,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
223222
KnownValue::new(1, 2, 1),
224223
],
225224
);
226-
let source_config = vec![0, 0, 0, 0, 1, 1, 0, 2, 1, 1, 0, 1, 1, 1, 1, 1, 2, 0];
227-
let reduction = source.clone().reduce_to();
228-
let target_config = reduction.encode_source_solution(&source_config);
229-
crate::example_db::specs::rule_example_with_witness::<_, ILP<bool>>(
230-
source,
231-
SolutionPair {
232-
source_config,
233-
target_config,
234-
},
235-
)
225+
crate::example_db::specs::rule_example_via_ilp::<_, bool>(source)
236226
},
237227
}]
238228
}

src/rules/directedtwocommodityintegralflow_ilp.rs

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,6 @@ impl ReduceTo<ILP<i32>> for DirectedTwoCommodityIntegralFlow {
147147

148148
#[cfg(feature = "example-db")]
149149
pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
150-
use crate::export::SolutionPair;
151150
use crate::topology::DirectedGraph;
152151

153152
vec![crate::example_db::specs::RuleExampleSpec {
@@ -178,16 +177,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
178177
1,
179178
1,
180179
);
181-
// first 8 = f1, next 8 = f2
182-
// f1: arc(0,2)=1, arc(2,4)=1, rest=0 → [1,0,0,0,1,0,0,0]
183-
// f2: arc(1,3)=1, arc(3,5)=1, rest=0 → [0,0,0,1,0,0,0,1]
184-
crate::example_db::specs::rule_example_with_witness::<_, ILP<i32>>(
185-
source,
186-
SolutionPair {
187-
source_config: vec![1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1],
188-
target_config: vec![1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1],
189-
},
190-
)
180+
crate::example_db::specs::rule_example_via_ilp::<_, i32>(source)
191181
},
192182
}]
193183
}

src/rules/disjointconnectingpaths_ilp.rs

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,6 @@ impl ReduceTo<ILP<bool>> for DisjointConnectingPaths<SimpleGraph> {
170170

171171
#[cfg(feature = "example-db")]
172172
pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
173-
use crate::export::SolutionPair;
174-
use crate::rules::ReduceTo as _;
175-
176173
vec![crate::example_db::specs::RuleExampleSpec {
177174
id: "disjointconnectingpaths_to_ilp",
178175
build: || {
@@ -181,18 +178,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
181178
SimpleGraph::new(6, vec![(0, 1), (1, 2), (3, 4), (4, 5)]),
182179
vec![(0, 2), (3, 5)],
183180
);
184-
let reduction = ReduceTo::<ILP<bool>>::reduce_to(&source);
185-
let ilp_solution = crate::solvers::ILPSolver::new()
186-
.solve(reduction.target_problem())
187-
.expect("canonical example must be solvable");
188-
let source_config = reduction.extract_solution(&ilp_solution);
189-
crate::example_db::specs::rule_example_with_witness::<_, ILP<bool>>(
190-
source,
191-
SolutionPair {
192-
source_config,
193-
target_config: ilp_solution,
194-
},
195-
)
181+
crate::example_db::specs::rule_example_via_ilp::<_, bool>(source)
196182
},
197183
}]
198184
}

src/rules/factoring_ilp.rs

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -223,20 +223,11 @@ impl ReduceTo<ILP<i32>> for Factoring {
223223

224224
#[cfg(feature = "example-db")]
225225
pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
226-
use crate::export::SolutionPair;
227-
228226
vec![crate::example_db::specs::RuleExampleSpec {
229227
id: "factoring_to_ilp",
230228
build: || {
231-
crate::example_db::specs::rule_example_with_witness::<_, ILP<i32>>(
232-
Factoring::new(3, 3, 35),
233-
SolutionPair {
234-
source_config: vec![1, 0, 1, 1, 1, 1],
235-
target_config: vec![
236-
1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0,
237-
],
238-
},
239-
)
229+
let source = Factoring::new(3, 3, 35);
230+
crate::example_db::specs::rule_example_via_ilp::<_, i32>(source)
240231
},
241232
}]
242233
}

0 commit comments

Comments
 (0)