From abb24c697ba27dd6fff0e94c7445fe25d4b7ed3b Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Wed, 25 Feb 2026 10:26:35 +0800 Subject: [PATCH 1/6] Add plan for #95: BinPacking model Co-Authored-By: Claude Opus 4.6 --- docs/plans/2026-02-25-bin-packing.md | 472 +++++++++++++++++++++++++++ 1 file changed, 472 insertions(+) create mode 100644 docs/plans/2026-02-25-bin-packing.md diff --git a/docs/plans/2026-02-25-bin-packing.md b/docs/plans/2026-02-25-bin-packing.md new file mode 100644 index 00000000..86afbc83 --- /dev/null +++ b/docs/plans/2026-02-25-bin-packing.md @@ -0,0 +1,472 @@ +# BinPacking Model Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add a `BinPacking` optimization problem model that assigns items with sizes to bins of fixed capacity, minimizing the number of bins used. + +**Architecture:** `BinPacking` struct parameterized by weight type `W` (for item sizes). Configuration space is `vec![n; n]` — each of `n` items maps to one of `n` possible bins. The objective (number of distinct bins) is always `i32` regardless of `W`. Follows the `MaximumSetPacking` pattern (non-graph, weight-only variant). + +**Tech Stack:** Rust, serde, inventory, num-traits + +**Issue:** #95 + +--- + +### Task 1: Implement the BinPacking model + +**Files:** +- Create: `src/models/optimization/bin_packing.rs` +- Modify: `src/models/optimization/mod.rs` +- Modify: `src/models/mod.rs` + +**Step 1: Create `src/models/optimization/bin_packing.rs`** + +```rust +//! Bin Packing problem implementation. +//! +//! The Bin Packing problem asks for an assignment of items to bins +//! that minimizes the number of bins used while respecting capacity constraints. + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::traits::{OptimizationProblem, Problem}; +use crate::types::{Direction, SolutionSize, WeightElement}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "BinPacking", + module_path: module_path!(), + description: "Assign items to bins minimizing number of bins used, subject to capacity", + fields: &[ + FieldInfo { name: "sizes", type_name: "Vec", description: "Item sizes s_i for each item" }, + FieldInfo { name: "capacity", type_name: "W", description: "Bin capacity C" }, + ], + } +} + +/// The Bin Packing problem. +/// +/// Given `n` items with sizes `s_1, ..., s_n` and bin capacity `C`, +/// find an assignment of items to bins such that: +/// - For each bin `j`, the total size of items assigned to `j` does not exceed `C` +/// - The number of bins used is minimized +/// +/// # Representation +/// +/// Each item has a variable in `{0, ..., n-1}` representing its bin assignment. +/// The worst case uses `n` bins (one item per bin). +/// +/// # Type Parameters +/// +/// * `W` - The weight type for sizes and capacity (e.g., `i32`, `f64`) +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::optimization::BinPacking; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// // 4 items with sizes [3, 3, 2, 2], capacity 5 +/// let problem = BinPacking::new(vec![3, 3, 2, 2], 5); +/// let solver = BruteForce::new(); +/// let solution = solver.find_best(&problem); +/// assert!(solution.is_some()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BinPacking { + /// Item sizes. + sizes: Vec, + /// Bin capacity. + capacity: W, +} + +impl BinPacking { + /// Create a Bin Packing problem from item sizes and capacity. + pub fn new(sizes: Vec, capacity: W) -> Self { + Self { sizes, capacity } + } + + /// Get the item sizes. + pub fn sizes(&self) -> &[W] { + &self.sizes + } + + /// Get the bin capacity. + pub fn capacity(&self) -> &W { + &self.capacity + } + + /// Get the number of items. + pub fn num_items(&self) -> usize { + self.sizes.len() + } +} + +impl Problem for BinPacking +where + W: WeightElement + crate::variant::VariantParam, + W::Sum: PartialOrd, +{ + const NAME: &'static str = "BinPacking"; + type Metric = SolutionSize; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![W] + } + + fn dims(&self) -> Vec { + let n = self.sizes.len(); + vec![n; n] + } + + fn evaluate(&self, config: &[usize]) -> SolutionSize { + if !is_valid_packing(&self.sizes, &self.capacity, config) { + return SolutionSize::Invalid; + } + let num_bins = count_bins(config); + SolutionSize::Valid(num_bins as i32) + } + + fn problem_size_names() -> &'static [&'static str] { + &["num_items"] + } + fn problem_size_values(&self) -> Vec { + vec![self.num_items()] + } +} + +impl OptimizationProblem for BinPacking +where + W: WeightElement + crate::variant::VariantParam, + W::Sum: PartialOrd, +{ + type Value = i32; + + fn direction(&self) -> Direction { + Direction::Minimize + } +} + +/// Check if a configuration is a valid bin packing (all bins within capacity). +fn is_valid_packing(sizes: &[W], capacity: &W, config: &[usize]) -> bool +where + W::Sum: PartialOrd, +{ + if config.len() != sizes.len() { + return false; + } + let n = sizes.len(); + // Check all bin indices are in range + if config.iter().any(|&b| b >= n) { + return false; + } + // Compute load per bin + let cap_sum = capacity.to_sum(); + let mut bin_load: Vec = vec![W::Sum::default(); n]; + for (i, &bin) in config.iter().enumerate() { + bin_load[bin] += sizes[i].to_sum(); + } + // Check capacity constraints + bin_load.iter().all(|load| *load <= cap_sum) +} + +/// Count the number of distinct bins used in a configuration. +fn count_bins(config: &[usize]) -> usize { + let mut used = vec![false; config.len()]; + for &bin in config { + if bin < used.len() { + used[bin] = true; + } + } + used.iter().filter(|&&u| u).count() +} + +#[cfg(test)] +#[path = "../../unit_tests/models/optimization/bin_packing.rs"] +mod tests; +``` + +**Step 2: Register in `src/models/optimization/mod.rs`** + +Add after the existing module declarations: +```rust +pub(crate) mod bin_packing; +``` +Add to the public exports: +```rust +pub use bin_packing::BinPacking; +``` + +**Step 3: Register in `src/models/mod.rs`** + +Add `BinPacking` to the `optimization` re-export line: +```rust +pub use optimization::{BinPacking, SpinGlass, ILP, QUBO}; +``` + +**Step 4: Verify it compiles** + +Run: `make build` +Expected: Compiles with no errors (tests will fail since test file doesn't exist yet). + +**Step 5: Commit** + +```bash +git add src/models/optimization/bin_packing.rs src/models/optimization/mod.rs src/models/mod.rs +git commit -m "feat: add BinPacking model (optimization, minimize bins)" +``` + +--- + +### Task 2: Write unit tests + +**Files:** +- Create: `src/unit_tests/models/optimization/bin_packing.rs` + +Ensure parent directory exists — check if `src/unit_tests/models/optimization/` already has files (it should, from QUBO/ILP/SpinGlass). + +**Step 1: Create the test file** + +Reference: `src/unit_tests/models/graph/maximum_independent_set.rs` and `src/unit_tests/models/set/maximum_set_packing.rs`. + +```rust +use super::*; +use crate::solvers::BruteForce; +use crate::traits::{OptimizationProblem, Problem}; +use crate::types::Direction; + +#[test] +fn test_bin_packing_creation() { + let problem = BinPacking::new(vec![6, 6, 5, 5, 4, 4], 10); + assert_eq!(problem.num_items(), 6); + assert_eq!(problem.sizes(), &[6, 6, 5, 5, 4, 4]); + assert_eq!(*problem.capacity(), 10); + assert_eq!(problem.dims().len(), 6); + // Each variable has domain {0, ..., 5} + assert!(problem.dims().iter().all(|&d| d == 6)); +} + +#[test] +fn test_bin_packing_direction() { + let problem = BinPacking::new(vec![1, 2, 3], 5); + assert_eq!(problem.direction(), Direction::Minimize); +} + +#[test] +fn test_bin_packing_evaluate_valid() { + // 6 items, capacity 10, sizes [6, 6, 5, 5, 4, 4] + // Assignment: (0, 1, 2, 2, 0, 1) -> 3 bins + // Bin 0: items 0,4 -> 6+4=10 OK + // Bin 1: items 1,5 -> 6+4=10 OK + // Bin 2: items 2,3 -> 5+5=10 OK + let problem = BinPacking::new(vec![6, 6, 5, 5, 4, 4], 10); + let result = problem.evaluate(&[0, 1, 2, 2, 0, 1]); + assert!(result.is_valid()); + assert_eq!(result.unwrap(), 3); +} + +#[test] +fn test_bin_packing_evaluate_invalid_overweight() { + // Bin 0: items 0,1 -> 6+6=12 > 10 + let problem = BinPacking::new(vec![6, 6, 5, 5, 4, 4], 10); + let result = problem.evaluate(&[0, 0, 1, 1, 2, 2]); + assert!(!result.is_valid()); +} + +#[test] +fn test_bin_packing_evaluate_single_bin() { + // All items fit in one bin + let problem = BinPacking::new(vec![1, 2, 3], 10); + let result = problem.evaluate(&[0, 0, 0]); + assert!(result.is_valid()); + assert_eq!(result.unwrap(), 1); +} + +#[test] +fn test_bin_packing_evaluate_all_separate() { + // Each item in its own bin + let problem = BinPacking::new(vec![3, 3, 3], 5); + let result = problem.evaluate(&[0, 1, 2]); + assert!(result.is_valid()); + assert_eq!(result.unwrap(), 3); +} + +#[test] +fn test_bin_packing_problem_name() { + assert_eq!( as Problem>::NAME, "BinPacking"); +} + +#[test] +fn test_bin_packing_brute_force_solver() { + // 6 items, capacity 10, sizes [6, 6, 5, 5, 4, 4] + // Optimal: 3 bins (lower bound ceil(30/10) = 3) + let problem = BinPacking::new(vec![6, 6, 5, 5, 4, 4], 10); + let solver = BruteForce::new(); + let solution = solver.find_best(&problem).expect("should find a solution"); + let metric = problem.evaluate(&solution); + assert!(metric.is_valid()); + assert_eq!(metric.unwrap(), 3); +} + +#[test] +fn test_bin_packing_brute_force_small() { + // 3 items [3, 3, 4], capacity 7 + // Optimal: 2 bins (e.g., {3,4} + {3}) + let problem = BinPacking::new(vec![3, 3, 4], 7); + let solver = BruteForce::new(); + let solution = solver.find_best(&problem).expect("should find a solution"); + let metric = problem.evaluate(&solution); + assert!(metric.is_valid()); + assert_eq!(metric.unwrap(), 2); +} + +#[test] +fn test_bin_packing_serialization() { + let problem = BinPacking::new(vec![6, 6, 5, 5, 4, 4], 10); + let json = serde_json::to_value(&problem).unwrap(); + let restored: BinPacking = serde_json::from_value(json).unwrap(); + assert_eq!(restored.sizes(), problem.sizes()); + assert_eq!(restored.capacity(), problem.capacity()); +} +``` + +**Step 2: Check that the test directory exists** + +Run: `ls src/unit_tests/models/optimization/` +If the directory doesn't exist, check existing test patterns under `src/unit_tests/models/` and ensure there's a `mod.rs` that includes `bin_packing`. + +**Step 3: Run tests** + +Run: `cargo test bin_packing -- --nocapture` +Expected: All tests PASS. + +Note: The brute-force test with 6 items has search space 6^6 = 46656, which is tractable. If it's too slow, reduce to 4 items. + +**Step 4: Commit** + +```bash +git add src/unit_tests/models/optimization/bin_packing.rs +git commit -m "test: add BinPacking unit tests" +``` + +--- + +### Task 3: Register in CLI dispatch + +**Files:** +- Modify: `problemreductions-cli/src/dispatch.rs` +- Modify: `problemreductions-cli/src/problem_name.rs` + +**Step 1: Add match arms in `dispatch.rs`** + +In `load_problem()` (around line 207), add before the `_ => bail!` fallthrough: +```rust +"BinPacking" => match variant.get("weight").map(|s| s.as_str()) { + Some("f64") => deser_opt::>(data), + _ => deser_opt::>(data), +}, +``` + +In `serialize_any_problem()` (around line 257), add before the `_ => bail!` fallthrough: +```rust +"BinPacking" => match variant.get("weight").map(|s| s.as_str()) { + Some("f64") => try_ser::>(any), + _ => try_ser::>(any), +}, +``` + +Add import at the top of `dispatch.rs` if not already covered by `prelude::*`: +```rust +use problemreductions::models::optimization::BinPacking; +``` + +**Step 2: Add alias in `problem_name.rs`** + +In `resolve_alias()`, add: +```rust +"binpacking" => "BinPacking".to_string(), +``` + +Optionally add a short alias to the `ALIASES` array: +```rust +("BP", "BinPacking"), +``` + +**Step 3: Verify CLI builds** + +Run: `make cli` +Expected: Builds successfully. + +**Step 4: Commit** + +```bash +git add problemreductions-cli/src/dispatch.rs problemreductions-cli/src/problem_name.rs +git commit -m "feat: register BinPacking in CLI dispatch" +``` + +--- + +### Task 4: Add problem definition to paper + +**Files:** +- Modify: `docs/paper/reductions.typ` + +**Step 1: Add to `display-name` dictionary** + +Find the `display-name` dict (line ~28) and add: +```typst +"BinPacking": [Bin Packing], +``` + +**Step 2: Add `#problem-def` block** + +Add after an appropriate location (e.g., after TravelingSalesman or at the end of the optimization section): +```typst +#problem-def("BinPacking")[ + Given $n$ items with sizes $s_1, dots, s_n in RR^+$ and bin capacity $C > 0$, find an assignment $x: {1, dots, n} -> {1, dots, n}$ minimizing $|{x(i) : i = 1, dots, n}|$ (number of distinct bins used) subject to $forall j: sum_(i: x(i) = j) s_i lt.eq C$. +] +``` + +**Step 3: Commit** + +```bash +git add docs/paper/reductions.typ +git commit -m "docs: add BinPacking problem definition to paper" +``` + +--- + +### Task 5: Verify everything + +**Step 1: Run full check** + +Run: `make check` +Expected: fmt, clippy, and all tests pass. + +**Step 2: Run review-implementation skill** + +Use `/review-implementation` to verify structural and semantic completeness. + +**Step 3: Final commit if any fixups needed** + +--- + +## Summary of Files + +| Action | File | +|--------|------| +| Create | `src/models/optimization/bin_packing.rs` | +| Create | `src/unit_tests/models/optimization/bin_packing.rs` | +| Modify | `src/models/optimization/mod.rs` | +| Modify | `src/models/mod.rs` | +| Modify | `problemreductions-cli/src/dispatch.rs` | +| Modify | `problemreductions-cli/src/problem_name.rs` | +| Modify | `docs/paper/reductions.typ` | + +## Key Design Decisions + +1. **Category:** `optimization/` — BinPacking is a core optimization problem. +2. **Type parameter:** `W` only (no graph). Follows `MaximumSetPacking` pattern. +3. **Objective type:** `i32` always (bin count is integer), independent of `W`. +4. **Config space:** `vec![n; n]` — each of `n` items can be assigned to bins `0..n-1`. +5. **Feasibility:** Check per-bin load ≤ capacity. Out-of-range bin indices → invalid. From b6bc116640b3e255bbcb0cc4ec9366154e5adae8 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Wed, 25 Feb 2026 10:35:40 +0800 Subject: [PATCH 2/6] feat: add BinPacking model (optimization, minimize bins) Co-Authored-By: Claude Opus 4.6 --- src/models/mod.rs | 2 +- src/models/optimization/bin_packing.rs | 162 ++++++++++++++++++ src/models/optimization/mod.rs | 3 + .../models/optimization/bin_packing.rs | 2 + 4 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 src/models/optimization/bin_packing.rs create mode 100644 src/unit_tests/models/optimization/bin_packing.rs diff --git a/src/models/mod.rs b/src/models/mod.rs index 2b1bb93e..1f753b73 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -13,7 +13,7 @@ pub use graph::{ KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumVertexCover, TravelingSalesman, }; -pub use optimization::{SpinGlass, ILP, QUBO}; +pub use optimization::{BinPacking, SpinGlass, ILP, QUBO}; pub use satisfiability::{CNFClause, KSatisfiability, Satisfiability}; pub use set::{MaximumSetPacking, MinimumSetCovering}; pub use specialized::{BicliqueCover, CircuitSAT, Factoring, PaintShop, BMF}; diff --git a/src/models/optimization/bin_packing.rs b/src/models/optimization/bin_packing.rs new file mode 100644 index 00000000..b5d6c20d --- /dev/null +++ b/src/models/optimization/bin_packing.rs @@ -0,0 +1,162 @@ +//! Bin Packing problem implementation. +//! +//! The Bin Packing problem asks for an assignment of items to bins +//! that minimizes the number of bins used while respecting capacity constraints. + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::traits::{OptimizationProblem, Problem}; +use crate::types::{Direction, SolutionSize, WeightElement}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "BinPacking", + module_path: module_path!(), + description: "Assign items to bins minimizing number of bins used, subject to capacity", + fields: &[ + FieldInfo { name: "sizes", type_name: "Vec", description: "Item sizes s_i for each item" }, + FieldInfo { name: "capacity", type_name: "W", description: "Bin capacity C" }, + ], + } +} + +/// The Bin Packing problem. +/// +/// Given `n` items with sizes `s_1, ..., s_n` and bin capacity `C`, +/// find an assignment of items to bins such that: +/// - For each bin `j`, the total size of items assigned to `j` does not exceed `C` +/// - The number of bins used is minimized +/// +/// # Representation +/// +/// Each item has a variable in `{0, ..., n-1}` representing its bin assignment. +/// The worst case uses `n` bins (one item per bin). +/// +/// # Type Parameters +/// +/// * `W` - The weight type for sizes and capacity (e.g., `i32`, `f64`) +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::optimization::BinPacking; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// // 4 items with sizes [3, 3, 2, 2], capacity 5 +/// let problem = BinPacking::new(vec![3, 3, 2, 2], 5); +/// let solver = BruteForce::new(); +/// let solution = solver.find_best(&problem); +/// assert!(solution.is_some()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BinPacking { + /// Item sizes. + sizes: Vec, + /// Bin capacity. + capacity: W, +} + +impl BinPacking { + /// Create a Bin Packing problem from item sizes and capacity. + pub fn new(sizes: Vec, capacity: W) -> Self { + Self { sizes, capacity } + } + + /// Get the item sizes. + pub fn sizes(&self) -> &[W] { + &self.sizes + } + + /// Get the bin capacity. + pub fn capacity(&self) -> &W { + &self.capacity + } + + /// Get the number of items. + pub fn num_items(&self) -> usize { + self.sizes.len() + } +} + +impl Problem for BinPacking +where + W: WeightElement + crate::variant::VariantParam, + W::Sum: PartialOrd, +{ + const NAME: &'static str = "BinPacking"; + type Metric = SolutionSize; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![W] + } + + fn dims(&self) -> Vec { + let n = self.sizes.len(); + vec![n; n] + } + + fn evaluate(&self, config: &[usize]) -> SolutionSize { + if !is_valid_packing(&self.sizes, &self.capacity, config) { + return SolutionSize::Invalid; + } + let num_bins = count_bins(config); + SolutionSize::Valid(num_bins as i32) + } + + fn problem_size_names() -> &'static [&'static str] { + &["num_items"] + } + fn problem_size_values(&self) -> Vec { + vec![self.num_items()] + } +} + +impl OptimizationProblem for BinPacking +where + W: WeightElement + crate::variant::VariantParam, + W::Sum: PartialOrd, +{ + type Value = i32; + + fn direction(&self) -> Direction { + Direction::Minimize + } +} + +/// Check if a configuration is a valid bin packing (all bins within capacity). +fn is_valid_packing(sizes: &[W], capacity: &W, config: &[usize]) -> bool +where + W::Sum: PartialOrd, +{ + if config.len() != sizes.len() { + return false; + } + let n = sizes.len(); + // Check all bin indices are in range + if config.iter().any(|&b| b >= n) { + return false; + } + // Compute load per bin + let cap_sum = capacity.to_sum(); + let mut bin_load: Vec = vec![W::Sum::default(); n]; + for (i, &bin) in config.iter().enumerate() { + bin_load[bin] += sizes[i].to_sum(); + } + // Check capacity constraints + bin_load.iter().all(|load| *load <= cap_sum) +} + +/// Count the number of distinct bins used in a configuration. +fn count_bins(config: &[usize]) -> usize { + let mut used = vec![false; config.len()]; + for &bin in config { + if bin < used.len() { + used[bin] = true; + } + } + used.iter().filter(|&&u| u).count() +} + +#[cfg(test)] +#[path = "../../unit_tests/models/optimization/bin_packing.rs"] +mod tests; diff --git a/src/models/optimization/mod.rs b/src/models/optimization/mod.rs index cbee429f..53adb957 100644 --- a/src/models/optimization/mod.rs +++ b/src/models/optimization/mod.rs @@ -1,14 +1,17 @@ //! Optimization problems. //! //! This module contains optimization problems: +//! - [`BinPacking`]: Bin Packing (minimize bins) //! - [`SpinGlass`]: Ising model Hamiltonian //! - [`QUBO`]: Quadratic Unconstrained Binary Optimization //! - [`ILP`]: Integer Linear Programming +pub(crate) mod bin_packing; mod ilp; mod qubo; mod spin_glass; +pub use bin_packing::BinPacking; pub use ilp::{Comparison, LinearConstraint, ObjectiveSense, VarBounds, ILP}; pub use qubo::QUBO; pub use spin_glass::SpinGlass; diff --git a/src/unit_tests/models/optimization/bin_packing.rs b/src/unit_tests/models/optimization/bin_packing.rs new file mode 100644 index 00000000..69a61a0b --- /dev/null +++ b/src/unit_tests/models/optimization/bin_packing.rs @@ -0,0 +1,2 @@ +// Unit tests for BinPacking model. +// Full tests will be added in a subsequent task. From 7a3a1800b69d9ef9c334192b20cf78404a04a81b Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Wed, 25 Feb 2026 10:43:45 +0800 Subject: [PATCH 3/6] test: add BinPacking unit tests Co-Authored-By: Claude Opus 4.6 --- .../models/optimization/bin_packing.rs | 100 +++++++++++++++++- 1 file changed, 98 insertions(+), 2 deletions(-) diff --git a/src/unit_tests/models/optimization/bin_packing.rs b/src/unit_tests/models/optimization/bin_packing.rs index 69a61a0b..7c058c24 100644 --- a/src/unit_tests/models/optimization/bin_packing.rs +++ b/src/unit_tests/models/optimization/bin_packing.rs @@ -1,2 +1,98 @@ -// Unit tests for BinPacking model. -// Full tests will be added in a subsequent task. +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::traits::{OptimizationProblem, Problem}; +use crate::types::Direction; + +#[test] +fn test_bin_packing_creation() { + let problem = BinPacking::new(vec![6, 6, 5, 5, 4, 4], 10); + assert_eq!(problem.num_items(), 6); + assert_eq!(problem.sizes(), &[6, 6, 5, 5, 4, 4]); + assert_eq!(*problem.capacity(), 10); + assert_eq!(problem.dims().len(), 6); + // Each variable has domain {0, ..., 5} + assert!(problem.dims().iter().all(|&d| d == 6)); +} + +#[test] +fn test_bin_packing_direction() { + let problem = BinPacking::new(vec![1, 2, 3], 5); + assert_eq!(problem.direction(), Direction::Minimize); +} + +#[test] +fn test_bin_packing_evaluate_valid() { + // 6 items, capacity 10, sizes [6, 6, 5, 5, 4, 4] + // Assignment: (0, 1, 2, 2, 0, 1) -> 3 bins + // Bin 0: items 0,4 -> 6+4=10 OK + // Bin 1: items 1,5 -> 6+4=10 OK + // Bin 2: items 2,3 -> 5+5=10 OK + let problem = BinPacking::new(vec![6, 6, 5, 5, 4, 4], 10); + let result = problem.evaluate(&[0, 1, 2, 2, 0, 1]); + assert!(result.is_valid()); + assert_eq!(result.unwrap(), 3); +} + +#[test] +fn test_bin_packing_evaluate_invalid_overweight() { + // Bin 0: items 0,1 -> 6+6=12 > 10 + let problem = BinPacking::new(vec![6, 6, 5, 5, 4, 4], 10); + let result = problem.evaluate(&[0, 0, 1, 1, 2, 2]); + assert!(!result.is_valid()); +} + +#[test] +fn test_bin_packing_evaluate_single_bin() { + // All items fit in one bin + let problem = BinPacking::new(vec![1, 2, 3], 10); + let result = problem.evaluate(&[0, 0, 0]); + assert!(result.is_valid()); + assert_eq!(result.unwrap(), 1); +} + +#[test] +fn test_bin_packing_evaluate_all_separate() { + // Each item in its own bin + let problem = BinPacking::new(vec![3, 3, 3], 5); + let result = problem.evaluate(&[0, 1, 2]); + assert!(result.is_valid()); + assert_eq!(result.unwrap(), 3); +} + +#[test] +fn test_bin_packing_problem_name() { + assert_eq!( as Problem>::NAME, "BinPacking"); +} + +#[test] +fn test_bin_packing_brute_force_solver() { + // 6 items, capacity 10, sizes [6, 6, 5, 5, 4, 4] + // Optimal: 3 bins (lower bound ceil(30/10) = 3) + let problem = BinPacking::new(vec![6, 6, 5, 5, 4, 4], 10); + let solver = BruteForce::new(); + let solution = solver.find_best(&problem).expect("should find a solution"); + let metric = problem.evaluate(&solution); + assert!(metric.is_valid()); + assert_eq!(metric.unwrap(), 3); +} + +#[test] +fn test_bin_packing_brute_force_small() { + // 3 items [3, 3, 4], capacity 7 + // Optimal: 2 bins (e.g., {3,4} + {3}) + let problem = BinPacking::new(vec![3, 3, 4], 7); + let solver = BruteForce::new(); + let solution = solver.find_best(&problem).expect("should find a solution"); + let metric = problem.evaluate(&solution); + assert!(metric.is_valid()); + assert_eq!(metric.unwrap(), 2); +} + +#[test] +fn test_bin_packing_serialization() { + let problem = BinPacking::new(vec![6, 6, 5, 5, 4, 4], 10); + let json = serde_json::to_value(&problem).unwrap(); + let restored: BinPacking = serde_json::from_value(json).unwrap(); + assert_eq!(restored.sizes(), problem.sizes()); + assert_eq!(restored.capacity(), problem.capacity()); +} From a4040e1a0d5f3ed576092f069dbcc760f875dd66 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Wed, 25 Feb 2026 10:45:47 +0800 Subject: [PATCH 4/6] feat: register BinPacking in CLI dispatch Co-Authored-By: Claude Opus 4.6 --- problemreductions-cli/src/dispatch.rs | 10 +++++++++- problemreductions-cli/src/problem_name.rs | 2 ++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 285050b7..ac8a1cb9 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -1,5 +1,5 @@ use anyhow::{bail, Context, Result}; -use problemreductions::models::optimization::ILP; +use problemreductions::models::optimization::{BinPacking, ILP}; use problemreductions::prelude::*; use problemreductions::rules::{MinimizeSteps, ReductionGraph}; use problemreductions::solvers::{BruteForce, ILPSolver, Solver}; @@ -243,6 +243,10 @@ pub fn load_problem( "BicliqueCover" => deser_opt::(data), "BMF" => deser_opt::(data), "PaintShop" => deser_opt::(data), + "BinPacking" => match variant.get("weight").map(|s| s.as_str()) { + Some("f64") => deser_opt::>(data), + _ => deser_opt::>(data), + }, _ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)), } } @@ -294,6 +298,10 @@ pub fn serialize_any_problem( "BicliqueCover" => try_ser::(any), "BMF" => try_ser::(any), "PaintShop" => try_ser::(any), + "BinPacking" => match variant.get("weight").map(|s| s.as_str()) { + Some("f64") => try_ser::>(any), + _ => try_ser::>(any), + }, _ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)), } } diff --git a/problemreductions-cli/src/problem_name.rs b/problemreductions-cli/src/problem_name.rs index e40fac2a..43d46853 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -19,6 +19,7 @@ pub const ALIASES: &[(&str, &str)] = &[ ("3SAT", "KSatisfiability"), ("KSAT", "KSatisfiability"), ("TSP", "TravelingSalesman"), + ("BP", "BinPacking"), ]; /// Resolve a short alias to the canonical problem name. @@ -47,6 +48,7 @@ pub fn resolve_alias(input: &str) -> String { "paintshop" => "PaintShop".to_string(), "bmf" => "BMF".to_string(), "bicliquecover" => "BicliqueCover".to_string(), + "binpacking" => "BinPacking".to_string(), _ => input.to_string(), // pass-through for exact names } } From 544c967bab5848f3430c6e162e66a68901e43c22 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Wed, 25 Feb 2026 10:49:35 +0800 Subject: [PATCH 5/6] docs: add BinPacking problem definition to paper Also fix pub(crate) -> mod consistency in optimization/mod.rs. Co-Authored-By: Claude Opus 4.6 --- docs/paper/reductions.typ | 5 +++++ src/models/optimization/mod.rs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index cfae33fe..76a39646 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -49,6 +49,7 @@ "BMF": [Boolean Matrix Factorization], "PaintShop": [Paint Shop], "BicliqueCover": [Biclique Cover], + "BinPacking": [Bin Packing], ) // Definition label: "def:" — each definition block must have a matching label @@ -400,6 +401,10 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| Given a bipartite graph $G = (L, R, E)$ and integer $k$, find $k$ bicliques $(L_1, R_1), dots, (L_k, R_k)$ that cover all edges ($E subset.eq union.big_i L_i times R_i$) while minimizing the total size $sum_i (|L_i| + |R_i|)$. ] +#problem-def("BinPacking")[ + Given $n$ items with sizes $s_1, dots, s_n in RR^+$ and bin capacity $C > 0$, find an assignment $x: {1, dots, n} -> {1, dots, n}$ minimizing $|{x(i) : i = 1, dots, n}|$ (number of distinct bins used) subject to $forall j: sum_(i: x(i) = j) s_i lt.eq C$. +] + // Completeness check: warn about problem types in JSON but missing from paper #{ let json-models = { diff --git a/src/models/optimization/mod.rs b/src/models/optimization/mod.rs index 53adb957..6e86fc48 100644 --- a/src/models/optimization/mod.rs +++ b/src/models/optimization/mod.rs @@ -6,7 +6,7 @@ //! - [`QUBO`]: Quadratic Unconstrained Binary Optimization //! - [`ILP`]: Integer Linear Programming -pub(crate) mod bin_packing; +mod bin_packing; mod ilp; mod qubo; mod spin_glass; From 252eee8dba83d55223ea9033ee01dd03f74b4d9e Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Wed, 25 Feb 2026 22:47:15 +0800 Subject: [PATCH 6/6] update --- .../unitdiskmapping/pathdecomposition.rs | 7 ++++++- .../unitdiskmapping/pathdecomposition.rs | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/rules/unitdiskmapping/pathdecomposition.rs b/src/rules/unitdiskmapping/pathdecomposition.rs index cc930fde..6003241d 100644 --- a/src/rules/unitdiskmapping/pathdecomposition.rs +++ b/src/rules/unitdiskmapping/pathdecomposition.rs @@ -395,7 +395,10 @@ impl PathDecompositionMethod { /// Create a greedy method with specified number of restarts. pub fn greedy_with_restarts(nrepeat: usize) -> Self { - PathDecompositionMethod::Greedy { nrepeat } + // Zero restarts would skip greedy_decompose entirely and produce an empty layout. + PathDecompositionMethod::Greedy { + nrepeat: nrepeat.max(1), + } } } @@ -433,6 +436,8 @@ pub fn pathwidth( }; match method { PathDecompositionMethod::Greedy { nrepeat } => { + // Defend against direct enum construction with nrepeat = 0. + let nrepeat = nrepeat.max(1); let mut best: Option = None; for _ in 0..nrepeat { let layout = greedy_decompose(num_vertices, edges); diff --git a/src/unit_tests/rules/unitdiskmapping/pathdecomposition.rs b/src/unit_tests/rules/unitdiskmapping/pathdecomposition.rs index af282a73..0f7191dc 100644 --- a/src/unit_tests/rules/unitdiskmapping/pathdecomposition.rs +++ b/src/unit_tests/rules/unitdiskmapping/pathdecomposition.rs @@ -103,6 +103,26 @@ fn test_pathwidth_greedy() { assert_eq!(layout.vsep(), 1); } +#[test] +fn test_greedy_with_restarts_zero_clamps_to_one() { + match PathDecompositionMethod::greedy_with_restarts(0) { + PathDecompositionMethod::Greedy { nrepeat } => assert_eq!(nrepeat, 1), + _ => panic!("expected Greedy variant"), + } +} + +#[test] +fn test_pathwidth_greedy_zero_restarts_produces_complete_layout() { + let n = 5; + let edges: Vec<(usize, usize)> = (0..n - 1).map(|i| (i, i + 1)).collect(); + let layout = pathwidth(n, &edges, PathDecompositionMethod::Greedy { nrepeat: 0 }); + + assert_eq!(layout.vertices.len(), n); + assert_eq!(layout.vsep(), 1); + let verified = verify_vsep(n, &edges, &layout.vertices); + assert_eq!(verified, layout.vsep()); +} + #[test] fn test_pathwidth_minhthi() { let edges = vec![(0, 1), (1, 2)];