diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index ab67b7c18..b1aeec56f 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -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.], diff --git a/examples/reduction_binpacking_to_ilp.rs b/examples/reduction_binpacking_to_ilp.rs new file mode 100644 index 000000000..104d707a4 --- /dev/null +++ b/examples/reduction_binpacking_to_ilp.rs @@ -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::>::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::::variant()); + let target_variant = variant_to_map(ILP::::variant()); + let overhead = lookup_overhead( + "BinPacking", + &source_variant, + "ILP", + &target_variant, + ) + .unwrap_or_default(); + + let data = ReductionData { + source: ProblemSide { + problem: BinPacking::::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::::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() +} diff --git a/src/rules/binpacking_ilp.rs b/src/rules/binpacking_ilp.rs new file mode 100644 index 000000000..b44e29da3 --- /dev/null +++ b/src/rules/binpacking_ilp.rs @@ -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, + /// Number of items in the source problem. + n: usize, +} + +impl ReductionResult for ReductionBPToILP { + type Source = BinPacking; + type Target = ILP; + + fn target_problem(&self) -> &ILP { + &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 { + 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> for BinPacking { + 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; diff --git a/src/rules/mod.rs b/src/rules/mod.rs index c37af7e0a..2a2fd8702 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -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")] diff --git a/src/unit_tests/rules/binpacking_ilp.rs b/src/unit_tests/rules/binpacking_ilp.rs new file mode 100644 index 000000000..87633735f --- /dev/null +++ b/src/unit_tests/rules/binpacking_ilp.rs @@ -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::>::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::>::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::>::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::>::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::>::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::>::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::>::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 = 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)); +} diff --git a/tests/suites/examples.rs b/tests/suites/examples.rs index 784cf3432..665b033fb 100644 --- a/tests/suites/examples.rs +++ b/tests/suites/examples.rs @@ -12,6 +12,7 @@ macro_rules! example_test { example_test!(chained_reduction_factoring_to_spinglass); example_test!(chained_reduction_ksat_to_mis); +example_test!(reduction_binpacking_to_ilp); example_test!(reduction_circuitsat_to_ilp); example_test!(reduction_circuitsat_to_spinglass); example_test!(reduction_factoring_to_circuitsat); @@ -67,6 +68,7 @@ example_fn!( test_chained_reduction_ksat_to_mis, chained_reduction_ksat_to_mis ); +example_fn!(test_binpacking_to_ilp, reduction_binpacking_to_ilp); example_fn!(test_circuitsat_to_ilp, reduction_circuitsat_to_ilp); example_fn!( test_circuitsat_to_spinglass,