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
16 changes: 16 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -1644,6 +1644,22 @@ The following reductions to Integer Linear Programming are straightforward formu
_Solution extraction._ $K = {v : x_v = 1}$.
]

#reduction-rule("BinPacking", "ILP")[
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.
][
_Construction._ Given $n$ items with sizes $s_1, dots, s_n$ and bin capacity $C$:

_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.

_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).

_Objective:_ Minimize $sum_(j=0)^(n-1) y_j$.

_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.

_Solution extraction._ For each item $i$, find the unique $j$ with $x_(i j) = 1$; assign item $i$ to bin $j$.
]

#reduction-rule("TravelingSalesman", "ILP",
example: true,
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.],
Expand Down
115 changes: 115 additions & 0 deletions examples/reduction_binpacking_to_ilp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// # Bin Packing to ILP Reduction
//
// ## Mathematical Formulation
// Variables: x_{ij} in {0,1} (item i in bin j), y_j in {0,1} (bin j used).
// Constraints:
// Assignment: sum_j x_{ij} = 1 for each item i.
// Capacity: sum_i w_i * x_{ij} <= C * y_j for each bin j.
// Objective: minimize sum_j y_j.
//
// ## This Example
// - Instance: 5 items with weights [6, 5, 5, 4, 3], bin capacity 10
// - Optimal: 3 bins (e.g., {6,4}, {5,5}, {3})
// - Target ILP: 30 binary variables (25 assignment + 5 bin-open), 10 constraints
//
// ## Output
// Exports `docs/paper/examples/binpacking_to_ilp.json` and `binpacking_to_ilp.result.json`.

use problemreductions::export::*;
use problemreductions::models::algebraic::ILP;
use problemreductions::prelude::*;
use problemreductions::solvers::ILPSolver;
use problemreductions::types::SolutionSize;

pub fn run() {
// 1. Create BinPacking instance: 5 items, capacity 10
let weights = vec![6, 5, 5, 4, 3];
let capacity = 10;
let bp = BinPacking::new(weights.clone(), capacity);

// 2. Reduce to ILP
let reduction = ReduceTo::<ILP<bool>>::reduce_to(&bp);
let ilp = reduction.target_problem();

// 3. Print transformation
println!("\n=== Problem Transformation ===");
println!(
"Source: BinPacking with {} items, weights {:?}, capacity {}",
bp.num_items(),
bp.sizes(),
bp.capacity()
);
println!(
"Target: ILP with {} variables, {} constraints",
ilp.num_vars,
ilp.constraints.len()
);

// 4. Solve target ILP using ILP solver (BruteForce would be too slow: 2^30 configs)
let ilp_solver = ILPSolver::new();
let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable");

println!("\n=== Solution ===");

// 5. Extract source solution
let bp_solution = reduction.extract_solution(&ilp_solution);
println!("Source BinPacking solution (bin assignments): {:?}", bp_solution);

// 6. Verify
let size = bp.evaluate(&bp_solution);
println!("Number of bins used: {:?}", size);
assert!(size.is_valid());
assert_eq!(size, SolutionSize::Valid(3));
println!("\nReduction verified successfully");

// 7. Collect solution and export JSON
let mut solutions = Vec::new();
{
let source_sol = reduction.extract_solution(&ilp_solution);
let s = bp.evaluate(&source_sol);
assert!(s.is_valid());
solutions.push(SolutionPair {
source_config: source_sol,
target_config: ilp_solution.clone(),
});
}

let source_variant = variant_to_map(BinPacking::<i32>::variant());
let target_variant = variant_to_map(ILP::<bool>::variant());
let overhead = lookup_overhead(
"BinPacking",
&source_variant,
"ILP",
&target_variant,
)
.unwrap_or_default();

let data = ReductionData {
source: ProblemSide {
problem: BinPacking::<i32>::NAME.to_string(),
variant: source_variant,
instance: serde_json::json!({
"num_items": bp.num_items(),
"sizes": bp.sizes(),
"capacity": bp.capacity(),
}),
},
target: ProblemSide {
problem: ILP::<bool>::NAME.to_string(),
variant: target_variant,
instance: serde_json::json!({
"num_vars": ilp.num_vars,
"num_constraints": ilp.constraints.len(),
}),
},
overhead: overhead_to_json(&overhead),
};

let results = ResultData { solutions };
let name = "binpacking_to_ilp";
write_example(name, &data, &results);
}

fn main() {
run()
}
101 changes: 101 additions & 0 deletions src/rules/binpacking_ilp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//! Reduction from BinPacking to ILP (Integer Linear Programming).
//!
//! The Bin Packing problem can be formulated as a binary ILP using
//! the standard assignment formulation (Martello & Toth, 1990):
//! - Variables: `x_{ij}` (item i assigned to bin j) + `y_j` (bin j used), all binary
//! - Constraints: assignment (each item in exactly one bin) + capacity/linking
//! - Objective: minimize number of bins used

use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP};
use crate::models::misc::BinPacking;
use crate::reduction;
use crate::rules::traits::{ReduceTo, ReductionResult};

/// Result of reducing BinPacking to ILP.
///
/// Variable layout (all binary):
/// - `x_{ij}` for i=0..n-1, j=0..n-1: item i assigned to bin j (index: i*n + j)
/// - `y_j` for j=0..n-1: bin j is used (index: n*n + j)
///
/// Total: n^2 + n variables.
#[derive(Debug, Clone)]
pub struct ReductionBPToILP {
target: ILP<bool>,
/// Number of items in the source problem.
n: usize,
}

impl ReductionResult for ReductionBPToILP {
type Source = BinPacking<i32>;
type Target = ILP<bool>;

fn target_problem(&self) -> &ILP<bool> {
&self.target
}

/// Extract solution from ILP back to BinPacking.
///
/// For each item i, find the unique bin j where x_{ij} = 1.
fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
let n = self.n;
let mut assignment = vec![0usize; n];
for i in 0..n {
for j in 0..n {
if target_solution[i * n + j] == 1 {
assignment[i] = j;
break;
}
}
}
assignment
}
}

#[reduction(
overhead = {
num_vars = "num_items * num_items + num_items",
num_constraints = "2 * num_items",
}
)]
impl ReduceTo<ILP<bool>> for BinPacking<i32> {
type Result = ReductionBPToILP;

fn reduce_to(&self) -> Self::Result {
let n = self.num_items();
let num_vars = n * n + n;

let mut constraints = Vec::with_capacity(2 * n);

// Assignment constraints: for each item i, sum_j x_{ij} = 1
for i in 0..n {
let terms: Vec<(usize, f64)> = (0..n).map(|j| (i * n + j, 1.0)).collect();
constraints.push(LinearConstraint::eq(terms, 1.0));
}

// Capacity + linking constraints: for each bin j,
// sum_i w_i * x_{ij} - C * y_j <= 0
let cap = *self.capacity() as f64;
for j in 0..n {
let mut terms: Vec<(usize, f64)> = self
.sizes()
.iter()
.enumerate()
.map(|(i, w)| (i * n + j, *w as f64))
.collect();
// Subtract C * y_j
terms.push((n * n + j, -cap));
constraints.push(LinearConstraint::le(terms, 0.0));
}

// Objective: minimize sum_j y_j
let objective: Vec<(usize, f64)> = (0..n).map(|j| (n * n + j, 1.0)).collect();

let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize);

ReductionBPToILP { target, n }
}
}

#[cfg(test)]
#[path = "../unit_tests/rules/binpacking_ilp.rs"]
mod tests;
2 changes: 2 additions & 0 deletions src/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ mod traits;

pub mod unitdiskmapping;

#[cfg(feature = "ilp-solver")]
mod binpacking_ilp;
#[cfg(feature = "ilp-solver")]
mod circuit_ilp;
#[cfg(feature = "ilp-solver")]
Expand Down
143 changes: 143 additions & 0 deletions src/unit_tests/rules/binpacking_ilp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
use super::*;
use crate::solvers::{BruteForce, ILPSolver};
use crate::traits::Problem;
use crate::types::SolutionSize;

#[test]
fn test_reduction_creates_valid_ilp() {
// 3 items with weights [3, 3, 2], capacity 5
let problem = BinPacking::new(vec![3, 3, 2], 5);
let reduction: ReductionBPToILP = ReduceTo::<ILP<bool>>::reduce_to(&problem);
let ilp = reduction.target_problem();

// n=3: 9 assignment vars + 3 bin vars = 12
assert_eq!(ilp.num_vars, 12, "Should have n^2 + n variables");
// 3 assignment + 3 capacity = 6
assert_eq!(ilp.constraints.len(), 6, "Should have 2n constraints");
assert_eq!(ilp.sense, ObjectiveSense::Minimize, "Should minimize");
}

#[test]
fn test_binpacking_to_ilp_closed_loop() {
// 4 items with weights [3, 3, 2, 2], capacity 5
// Optimal: 2 bins, e.g. {3,2} and {3,2}
let problem = BinPacking::new(vec![3, 3, 2, 2], 5);
let reduction: ReductionBPToILP = ReduceTo::<ILP<bool>>::reduce_to(&problem);
let ilp = reduction.target_problem();

let bf = BruteForce::new();
let ilp_solver = ILPSolver::new();

// Solve original with brute force
let bf_solutions = bf.find_all_best(&problem);
let bf_obj = problem.evaluate(&bf_solutions[0]);

// Solve via ILP
let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable");
let extracted = reduction.extract_solution(&ilp_solution);
let ilp_obj = problem.evaluate(&extracted);

assert_eq!(bf_obj, SolutionSize::Valid(2));
assert_eq!(ilp_obj, SolutionSize::Valid(2));
}

#[test]
fn test_single_item() {
let problem = BinPacking::new(vec![5], 10);
let reduction: ReductionBPToILP = ReduceTo::<ILP<bool>>::reduce_to(&problem);
let ilp = reduction.target_problem();

assert_eq!(ilp.num_vars, 2); // 1 assignment + 1 bin var
assert_eq!(ilp.constraints.len(), 2); // 1 assignment + 1 capacity

let ilp_solver = ILPSolver::new();
let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable");
let extracted = reduction.extract_solution(&ilp_solution);

assert!(problem.evaluate(&extracted).is_valid());
assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(1));
}

#[test]
fn test_same_weight_items() {
// 4 items all weight 3, capacity 6 -> 2 items per bin -> 2 bins needed
let problem = BinPacking::new(vec![3, 3, 3, 3], 6);
let reduction: ReductionBPToILP = ReduceTo::<ILP<bool>>::reduce_to(&problem);
let ilp = reduction.target_problem();

let ilp_solver = ILPSolver::new();
let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable");
let extracted = reduction.extract_solution(&ilp_solution);

assert!(problem.evaluate(&extracted).is_valid());
assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(2));
}

#[test]
fn test_exact_fill() {
// 2 items, weights [5, 5], capacity 10 -> fit in 1 bin
let problem = BinPacking::new(vec![5, 5], 10);
let reduction: ReductionBPToILP = ReduceTo::<ILP<bool>>::reduce_to(&problem);
let ilp = reduction.target_problem();

let ilp_solver = ILPSolver::new();
let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable");
let extracted = reduction.extract_solution(&ilp_solution);

assert!(problem.evaluate(&extracted).is_valid());
assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(1));
}

#[test]
fn test_solution_extraction() {
let problem = BinPacking::new(vec![3, 3, 2], 5);
let reduction: ReductionBPToILP = ReduceTo::<ILP<bool>>::reduce_to(&problem);

// Manually construct an ILP solution:
// 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)
// y_0=1, y_1=1, y_2=0
let mut ilp_solution = vec![0usize; 12];
ilp_solution[0] = 1; // x_{0,0} = 1
ilp_solution[4] = 1; // x_{1,1} = 1
ilp_solution[6] = 1; // x_{2,0} = 1
ilp_solution[9] = 1; // y_0 = 1
ilp_solution[10] = 1; // y_1 = 1

let extracted = reduction.extract_solution(&ilp_solution);
assert_eq!(extracted, vec![0, 1, 0]);
assert!(problem.evaluate(&extracted).is_valid());
}

#[test]
fn test_ilp_structure_constraints() {
// 2 items, weights [3, 4], capacity 5
let problem = BinPacking::new(vec![3, 4], 5);
let reduction: ReductionBPToILP = ReduceTo::<ILP<bool>>::reduce_to(&problem);
let ilp = reduction.target_problem();

// 4 assignment vars + 2 bin vars = 6
assert_eq!(ilp.num_vars, 6);
// 2 assignment + 2 capacity = 4
assert_eq!(ilp.constraints.len(), 4);

// Check objective: minimize y_0 + y_1 (vars at indices 4 and 5)
let obj_vars: Vec<usize> = ilp.objective.iter().map(|&(v, _)| v).collect();
assert!(obj_vars.contains(&4));
assert!(obj_vars.contains(&5));
for &(_, coef) in &ilp.objective {
assert!((coef - 1.0).abs() < 1e-9);
}
}

#[test]
fn test_solve_reduced() {
let problem = BinPacking::new(vec![6, 5, 5, 4, 3], 10);

let ilp_solver = ILPSolver::new();
let solution = ilp_solver
.solve_reduced(&problem)
.expect("solve_reduced should work");

assert!(problem.evaluate(&solution).is_valid());
assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(3));
}
Loading
Loading