Skip to content

Commit 176919b

Browse files
zazabapclaudeGiggleLiu
authored
Fix #97: Add BinPacking to ILP reduction (#597)
* Add plan for #97: BinPacking to ILP reduction * Implement #97: BinPacking to ILP reduction Add the standard assignment-based formulation (Martello & Toth, 1990) for reducing Bin Packing to Integer Linear Programming. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: remove plan file after implementation --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: GiggleLiu <cacate0129@gmail.com>
1 parent e14d21e commit 176919b

6 files changed

Lines changed: 379 additions & 0 deletions

File tree

docs/paper/reductions.typ

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1644,6 +1644,22 @@ The following reductions to Integer Linear Programming are straightforward formu
16441644
_Solution extraction._ $K = {v : x_v = 1}$.
16451645
]
16461646

1647+
#reduction-rule("BinPacking", "ILP")[
1648+
The assignment-based formulation introduces a binary indicator for each item--bin pair and a binary variable for each bin being open. Assignment constraints ensure each item is placed in exactly one bin; capacity constraints link bin usage to item weights.
1649+
][
1650+
_Construction._ Given $n$ items with sizes $s_1, dots, s_n$ and bin capacity $C$:
1651+
1652+
_Variables:_ $x_(i j) in {0, 1}$ for $i, j in {0, dots, n-1}$: item $i$ is assigned to bin $j$. $y_j in {0, 1}$: bin $j$ is used. Total: $n^2 + n$ variables.
1653+
1654+
_Constraints:_ (1) Assignment: $sum_(j=0)^(n-1) x_(i j) = 1$ for each item $i$ (each item in exactly one bin). (2) Capacity + linking: $sum_(i=0)^(n-1) s_i dot x_(i j) lt.eq C dot y_j$ for each bin $j$ (bin capacity respected; $y_j$ forced to 1 if bin $j$ is used).
1655+
1656+
_Objective:_ Minimize $sum_(j=0)^(n-1) y_j$.
1657+
1658+
_Correctness._ ($arrow.r.double$) A valid packing assigns each item to exactly one bin (satisfying (1)); each bin's load is at most $C$ and $y_j = 1$ for any used bin (satisfying (2)). ($arrow.l.double$) Any feasible solution assigns each item to one bin by (1), respects capacity by (2), and the objective counts the number of open bins.
1659+
1660+
_Solution extraction._ For each item $i$, find the unique $j$ with $x_(i j) = 1$; assign item $i$ to bin $j$.
1661+
]
1662+
16471663
#reduction-rule("TravelingSalesman", "ILP",
16481664
example: true,
16491665
example-caption: [Weighted $K_4$: the optimal tour $0 arrow 1 arrow 3 arrow 2 arrow 0$ with cost 80 is found by position-based ILP.],
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// # Bin Packing to ILP Reduction
2+
//
3+
// ## Mathematical Formulation
4+
// Variables: x_{ij} in {0,1} (item i in bin j), y_j in {0,1} (bin j used).
5+
// Constraints:
6+
// Assignment: sum_j x_{ij} = 1 for each item i.
7+
// Capacity: sum_i w_i * x_{ij} <= C * y_j for each bin j.
8+
// Objective: minimize sum_j y_j.
9+
//
10+
// ## This Example
11+
// - Instance: 5 items with weights [6, 5, 5, 4, 3], bin capacity 10
12+
// - Optimal: 3 bins (e.g., {6,4}, {5,5}, {3})
13+
// - Target ILP: 30 binary variables (25 assignment + 5 bin-open), 10 constraints
14+
//
15+
// ## Output
16+
// Exports `docs/paper/examples/binpacking_to_ilp.json` and `binpacking_to_ilp.result.json`.
17+
18+
use problemreductions::export::*;
19+
use problemreductions::models::algebraic::ILP;
20+
use problemreductions::prelude::*;
21+
use problemreductions::solvers::ILPSolver;
22+
use problemreductions::types::SolutionSize;
23+
24+
pub fn run() {
25+
// 1. Create BinPacking instance: 5 items, capacity 10
26+
let weights = vec![6, 5, 5, 4, 3];
27+
let capacity = 10;
28+
let bp = BinPacking::new(weights.clone(), capacity);
29+
30+
// 2. Reduce to ILP
31+
let reduction = ReduceTo::<ILP<bool>>::reduce_to(&bp);
32+
let ilp = reduction.target_problem();
33+
34+
// 3. Print transformation
35+
println!("\n=== Problem Transformation ===");
36+
println!(
37+
"Source: BinPacking with {} items, weights {:?}, capacity {}",
38+
bp.num_items(),
39+
bp.sizes(),
40+
bp.capacity()
41+
);
42+
println!(
43+
"Target: ILP with {} variables, {} constraints",
44+
ilp.num_vars,
45+
ilp.constraints.len()
46+
);
47+
48+
// 4. Solve target ILP using ILP solver (BruteForce would be too slow: 2^30 configs)
49+
let ilp_solver = ILPSolver::new();
50+
let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable");
51+
52+
println!("\n=== Solution ===");
53+
54+
// 5. Extract source solution
55+
let bp_solution = reduction.extract_solution(&ilp_solution);
56+
println!("Source BinPacking solution (bin assignments): {:?}", bp_solution);
57+
58+
// 6. Verify
59+
let size = bp.evaluate(&bp_solution);
60+
println!("Number of bins used: {:?}", size);
61+
assert!(size.is_valid());
62+
assert_eq!(size, SolutionSize::Valid(3));
63+
println!("\nReduction verified successfully");
64+
65+
// 7. Collect solution and export JSON
66+
let mut solutions = Vec::new();
67+
{
68+
let source_sol = reduction.extract_solution(&ilp_solution);
69+
let s = bp.evaluate(&source_sol);
70+
assert!(s.is_valid());
71+
solutions.push(SolutionPair {
72+
source_config: source_sol,
73+
target_config: ilp_solution.clone(),
74+
});
75+
}
76+
77+
let source_variant = variant_to_map(BinPacking::<i32>::variant());
78+
let target_variant = variant_to_map(ILP::<bool>::variant());
79+
let overhead = lookup_overhead(
80+
"BinPacking",
81+
&source_variant,
82+
"ILP",
83+
&target_variant,
84+
)
85+
.unwrap_or_default();
86+
87+
let data = ReductionData {
88+
source: ProblemSide {
89+
problem: BinPacking::<i32>::NAME.to_string(),
90+
variant: source_variant,
91+
instance: serde_json::json!({
92+
"num_items": bp.num_items(),
93+
"sizes": bp.sizes(),
94+
"capacity": bp.capacity(),
95+
}),
96+
},
97+
target: ProblemSide {
98+
problem: ILP::<bool>::NAME.to_string(),
99+
variant: target_variant,
100+
instance: serde_json::json!({
101+
"num_vars": ilp.num_vars,
102+
"num_constraints": ilp.constraints.len(),
103+
}),
104+
},
105+
overhead: overhead_to_json(&overhead),
106+
};
107+
108+
let results = ResultData { solutions };
109+
let name = "binpacking_to_ilp";
110+
write_example(name, &data, &results);
111+
}
112+
113+
fn main() {
114+
run()
115+
}

src/rules/binpacking_ilp.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
//! Reduction from BinPacking to ILP (Integer Linear Programming).
2+
//!
3+
//! The Bin Packing problem can be formulated as a binary ILP using
4+
//! the standard assignment formulation (Martello & Toth, 1990):
5+
//! - Variables: `x_{ij}` (item i assigned to bin j) + `y_j` (bin j used), all binary
6+
//! - Constraints: assignment (each item in exactly one bin) + capacity/linking
7+
//! - Objective: minimize number of bins used
8+
9+
use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP};
10+
use crate::models::misc::BinPacking;
11+
use crate::reduction;
12+
use crate::rules::traits::{ReduceTo, ReductionResult};
13+
14+
/// Result of reducing BinPacking to ILP.
15+
///
16+
/// Variable layout (all binary):
17+
/// - `x_{ij}` for i=0..n-1, j=0..n-1: item i assigned to bin j (index: i*n + j)
18+
/// - `y_j` for j=0..n-1: bin j is used (index: n*n + j)
19+
///
20+
/// Total: n^2 + n variables.
21+
#[derive(Debug, Clone)]
22+
pub struct ReductionBPToILP {
23+
target: ILP<bool>,
24+
/// Number of items in the source problem.
25+
n: usize,
26+
}
27+
28+
impl ReductionResult for ReductionBPToILP {
29+
type Source = BinPacking<i32>;
30+
type Target = ILP<bool>;
31+
32+
fn target_problem(&self) -> &ILP<bool> {
33+
&self.target
34+
}
35+
36+
/// Extract solution from ILP back to BinPacking.
37+
///
38+
/// For each item i, find the unique bin j where x_{ij} = 1.
39+
fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
40+
let n = self.n;
41+
let mut assignment = vec![0usize; n];
42+
for i in 0..n {
43+
for j in 0..n {
44+
if target_solution[i * n + j] == 1 {
45+
assignment[i] = j;
46+
break;
47+
}
48+
}
49+
}
50+
assignment
51+
}
52+
}
53+
54+
#[reduction(
55+
overhead = {
56+
num_vars = "num_items * num_items + num_items",
57+
num_constraints = "2 * num_items",
58+
}
59+
)]
60+
impl ReduceTo<ILP<bool>> for BinPacking<i32> {
61+
type Result = ReductionBPToILP;
62+
63+
fn reduce_to(&self) -> Self::Result {
64+
let n = self.num_items();
65+
let num_vars = n * n + n;
66+
67+
let mut constraints = Vec::with_capacity(2 * n);
68+
69+
// Assignment constraints: for each item i, sum_j x_{ij} = 1
70+
for i in 0..n {
71+
let terms: Vec<(usize, f64)> = (0..n).map(|j| (i * n + j, 1.0)).collect();
72+
constraints.push(LinearConstraint::eq(terms, 1.0));
73+
}
74+
75+
// Capacity + linking constraints: for each bin j,
76+
// sum_i w_i * x_{ij} - C * y_j <= 0
77+
let cap = *self.capacity() as f64;
78+
for j in 0..n {
79+
let mut terms: Vec<(usize, f64)> = self
80+
.sizes()
81+
.iter()
82+
.enumerate()
83+
.map(|(i, w)| (i * n + j, *w as f64))
84+
.collect();
85+
// Subtract C * y_j
86+
terms.push((n * n + j, -cap));
87+
constraints.push(LinearConstraint::le(terms, 0.0));
88+
}
89+
90+
// Objective: minimize sum_j y_j
91+
let objective: Vec<(usize, f64)> = (0..n).map(|j| (n * n + j, 1.0)).collect();
92+
93+
let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize);
94+
95+
ReductionBPToILP { target, n }
96+
}
97+
}
98+
99+
#[cfg(test)]
100+
#[path = "../unit_tests/rules/binpacking_ilp.rs"]
101+
mod tests;

src/rules/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ mod traits;
3535

3636
pub mod unitdiskmapping;
3737

38+
#[cfg(feature = "ilp-solver")]
39+
mod binpacking_ilp;
3840
#[cfg(feature = "ilp-solver")]
3941
mod circuit_ilp;
4042
#[cfg(feature = "ilp-solver")]
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
use super::*;
2+
use crate::solvers::{BruteForce, ILPSolver};
3+
use crate::traits::Problem;
4+
use crate::types::SolutionSize;
5+
6+
#[test]
7+
fn test_reduction_creates_valid_ilp() {
8+
// 3 items with weights [3, 3, 2], capacity 5
9+
let problem = BinPacking::new(vec![3, 3, 2], 5);
10+
let reduction: ReductionBPToILP = ReduceTo::<ILP<bool>>::reduce_to(&problem);
11+
let ilp = reduction.target_problem();
12+
13+
// n=3: 9 assignment vars + 3 bin vars = 12
14+
assert_eq!(ilp.num_vars, 12, "Should have n^2 + n variables");
15+
// 3 assignment + 3 capacity = 6
16+
assert_eq!(ilp.constraints.len(), 6, "Should have 2n constraints");
17+
assert_eq!(ilp.sense, ObjectiveSense::Minimize, "Should minimize");
18+
}
19+
20+
#[test]
21+
fn test_binpacking_to_ilp_closed_loop() {
22+
// 4 items with weights [3, 3, 2, 2], capacity 5
23+
// Optimal: 2 bins, e.g. {3,2} and {3,2}
24+
let problem = BinPacking::new(vec![3, 3, 2, 2], 5);
25+
let reduction: ReductionBPToILP = ReduceTo::<ILP<bool>>::reduce_to(&problem);
26+
let ilp = reduction.target_problem();
27+
28+
let bf = BruteForce::new();
29+
let ilp_solver = ILPSolver::new();
30+
31+
// Solve original with brute force
32+
let bf_solutions = bf.find_all_best(&problem);
33+
let bf_obj = problem.evaluate(&bf_solutions[0]);
34+
35+
// Solve via ILP
36+
let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable");
37+
let extracted = reduction.extract_solution(&ilp_solution);
38+
let ilp_obj = problem.evaluate(&extracted);
39+
40+
assert_eq!(bf_obj, SolutionSize::Valid(2));
41+
assert_eq!(ilp_obj, SolutionSize::Valid(2));
42+
}
43+
44+
#[test]
45+
fn test_single_item() {
46+
let problem = BinPacking::new(vec![5], 10);
47+
let reduction: ReductionBPToILP = ReduceTo::<ILP<bool>>::reduce_to(&problem);
48+
let ilp = reduction.target_problem();
49+
50+
assert_eq!(ilp.num_vars, 2); // 1 assignment + 1 bin var
51+
assert_eq!(ilp.constraints.len(), 2); // 1 assignment + 1 capacity
52+
53+
let ilp_solver = ILPSolver::new();
54+
let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable");
55+
let extracted = reduction.extract_solution(&ilp_solution);
56+
57+
assert!(problem.evaluate(&extracted).is_valid());
58+
assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(1));
59+
}
60+
61+
#[test]
62+
fn test_same_weight_items() {
63+
// 4 items all weight 3, capacity 6 -> 2 items per bin -> 2 bins needed
64+
let problem = BinPacking::new(vec![3, 3, 3, 3], 6);
65+
let reduction: ReductionBPToILP = ReduceTo::<ILP<bool>>::reduce_to(&problem);
66+
let ilp = reduction.target_problem();
67+
68+
let ilp_solver = ILPSolver::new();
69+
let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable");
70+
let extracted = reduction.extract_solution(&ilp_solution);
71+
72+
assert!(problem.evaluate(&extracted).is_valid());
73+
assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(2));
74+
}
75+
76+
#[test]
77+
fn test_exact_fill() {
78+
// 2 items, weights [5, 5], capacity 10 -> fit in 1 bin
79+
let problem = BinPacking::new(vec![5, 5], 10);
80+
let reduction: ReductionBPToILP = ReduceTo::<ILP<bool>>::reduce_to(&problem);
81+
let ilp = reduction.target_problem();
82+
83+
let ilp_solver = ILPSolver::new();
84+
let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable");
85+
let extracted = reduction.extract_solution(&ilp_solution);
86+
87+
assert!(problem.evaluate(&extracted).is_valid());
88+
assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(1));
89+
}
90+
91+
#[test]
92+
fn test_solution_extraction() {
93+
let problem = BinPacking::new(vec![3, 3, 2], 5);
94+
let reduction: ReductionBPToILP = ReduceTo::<ILP<bool>>::reduce_to(&problem);
95+
96+
// Manually construct an ILP solution:
97+
// n=3, x_{00}=1 (item 0 in bin 0), x_{11}=1 (item 1 in bin 1), x_{20}=1 (item 2 in bin 0)
98+
// y_0=1, y_1=1, y_2=0
99+
let mut ilp_solution = vec![0usize; 12];
100+
ilp_solution[0] = 1; // x_{0,0} = 1
101+
ilp_solution[4] = 1; // x_{1,1} = 1
102+
ilp_solution[6] = 1; // x_{2,0} = 1
103+
ilp_solution[9] = 1; // y_0 = 1
104+
ilp_solution[10] = 1; // y_1 = 1
105+
106+
let extracted = reduction.extract_solution(&ilp_solution);
107+
assert_eq!(extracted, vec![0, 1, 0]);
108+
assert!(problem.evaluate(&extracted).is_valid());
109+
}
110+
111+
#[test]
112+
fn test_ilp_structure_constraints() {
113+
// 2 items, weights [3, 4], capacity 5
114+
let problem = BinPacking::new(vec![3, 4], 5);
115+
let reduction: ReductionBPToILP = ReduceTo::<ILP<bool>>::reduce_to(&problem);
116+
let ilp = reduction.target_problem();
117+
118+
// 4 assignment vars + 2 bin vars = 6
119+
assert_eq!(ilp.num_vars, 6);
120+
// 2 assignment + 2 capacity = 4
121+
assert_eq!(ilp.constraints.len(), 4);
122+
123+
// Check objective: minimize y_0 + y_1 (vars at indices 4 and 5)
124+
let obj_vars: Vec<usize> = ilp.objective.iter().map(|&(v, _)| v).collect();
125+
assert!(obj_vars.contains(&4));
126+
assert!(obj_vars.contains(&5));
127+
for &(_, coef) in &ilp.objective {
128+
assert!((coef - 1.0).abs() < 1e-9);
129+
}
130+
}
131+
132+
#[test]
133+
fn test_solve_reduced() {
134+
let problem = BinPacking::new(vec![6, 5, 5, 4, 3], 10);
135+
136+
let ilp_solver = ILPSolver::new();
137+
let solution = ilp_solver
138+
.solve_reduced(&problem)
139+
.expect("solve_reduced should work");
140+
141+
assert!(problem.evaluate(&solution).is_valid());
142+
assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(3));
143+
}

0 commit comments

Comments
 (0)