From 3a74983a3ffec5d74bd8db24d21d14579606a868 Mon Sep 17 00:00:00 2001 From: zazabap Date: Wed, 4 Mar 2026 19:06:55 +0000 Subject: [PATCH 1/9] Add plan for #114: Knapsack --- docs/plans/2026-03-04-knapsack-model.md | 420 ++++++++++++++++++++++++ 1 file changed, 420 insertions(+) create mode 100644 docs/plans/2026-03-04-knapsack-model.md diff --git a/docs/plans/2026-03-04-knapsack-model.md b/docs/plans/2026-03-04-knapsack-model.md new file mode 100644 index 000000000..26567cace --- /dev/null +++ b/docs/plans/2026-03-04-knapsack-model.md @@ -0,0 +1,420 @@ +# Knapsack Model Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add the Knapsack (0-1) problem model to the codebase as a new `misc/` category model. + +**Architecture:** Knapsack is a maximization problem with binary variables (select/don't select each item). It has no graph or weight type parameters — all fields are concrete `i64`. The struct stores `weights`, `values`, and `capacity`. Feasibility requires total weight ≤ capacity; objective is total value. Solved via existing BruteForce solver. + +**Tech Stack:** Rust, serde, inventory registration, `Problem`/`OptimizationProblem` traits. + +**Reference:** Follow `add-model` skill Steps 1–7. Reference files: `src/models/misc/bin_packing.rs`, `src/unit_tests/models/misc/bin_packing.rs`. + +--- + +### Task 1: Create the Knapsack model file + +**Files:** +- Create: `src/models/misc/knapsack.rs` +- Modify: `src/models/misc/mod.rs` + +**Step 1: Create `src/models/misc/knapsack.rs`** + +```rust +//! Knapsack problem implementation. +//! +//! The 0-1 Knapsack problem asks for a subset of items that maximizes +//! total value while respecting a weight capacity constraint. + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::traits::{OptimizationProblem, Problem}; +use crate::types::{Direction, SolutionSize}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "Knapsack", + module_path: module_path!(), + description: "Select items to maximize total value subject to weight capacity constraint", + fields: &[ + FieldInfo { name: "weights", type_name: "Vec", description: "Item weights w_i" }, + FieldInfo { name: "values", type_name: "Vec", description: "Item values v_i" }, + FieldInfo { name: "capacity", type_name: "i64", description: "Knapsack capacity C" }, + ], + } +} + +/// The 0-1 Knapsack problem. +/// +/// Given `n` items, each with weight `w_i` and value `v_i`, and a capacity `C`, +/// find a subset `S ⊆ {0, ..., n-1}` such that `∑_{i∈S} w_i ≤ C`, +/// maximizing `∑_{i∈S} v_i`. +/// +/// # Representation +/// +/// Each item has a binary variable: `x_i = 1` if item `i` is selected, `0` otherwise. +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::misc::Knapsack; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); +/// let solver = BruteForce::new(); +/// let solution = solver.find_best(&problem); +/// assert!(solution.is_some()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Knapsack { + weights: Vec, + values: Vec, + capacity: i64, +} + +impl Knapsack { + /// Create a new Knapsack instance. + /// + /// # Panics + /// Panics if `weights` and `values` have different lengths. + pub fn new(weights: Vec, values: Vec, capacity: i64) -> Self { + assert_eq!( + weights.len(), + values.len(), + "weights and values must have the same length" + ); + Self { + weights, + values, + capacity, + } + } + + /// Returns the item weights. + pub fn weights(&self) -> &[i64] { + &self.weights + } + + /// Returns the item values. + pub fn values(&self) -> &[i64] { + &self.values + } + + /// Returns the knapsack capacity. + pub fn capacity(&self) -> i64 { + self.capacity + } + + /// Returns the number of items. + pub fn num_items(&self) -> usize { + self.weights.len() + } +} + +impl Problem for Knapsack { + const NAME: &'static str = "Knapsack"; + type Metric = SolutionSize; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + vec![2; self.num_items()] + } + + fn evaluate(&self, config: &[usize]) -> SolutionSize { + if config.len() != self.num_items() { + return SolutionSize::Invalid; + } + if config.iter().any(|&v| v >= 2) { + return SolutionSize::Invalid; + } + let total_weight: i64 = config + .iter() + .enumerate() + .filter(|(_, &x)| x == 1) + .map(|(i, _)| self.weights[i]) + .sum(); + if total_weight > self.capacity { + return SolutionSize::Invalid; + } + let total_value: i64 = config + .iter() + .enumerate() + .filter(|(_, &x)| x == 1) + .map(|(i, _)| self.values[i]) + .sum(); + SolutionSize::Valid(total_value) + } +} + +impl OptimizationProblem for Knapsack { + type Value = i64; + + fn direction(&self) -> Direction { + Direction::Maximize + } +} + +crate::declare_variants! { + Knapsack => "2^(num_items / 2)", +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/knapsack.rs"] +mod tests; +``` + +**Step 2: Register in `src/models/misc/mod.rs`** + +Add `mod knapsack;` and `pub use knapsack::Knapsack;` following the existing pattern (alphabetical order). + +**Step 3: Verify it compiles** + +Run: `make build` +Expected: SUCCESS + +**Step 4: Commit** + +```bash +git add src/models/misc/knapsack.rs src/models/misc/mod.rs +git commit -m "feat: add Knapsack model struct and Problem impl" +``` + +--- + +### Task 2: Write unit tests for Knapsack + +**Files:** +- Create: `src/unit_tests/models/misc/knapsack.rs` +- Modify: `src/unit_tests/models/misc/mod.rs` (if it exists) + +**Step 1: Create `src/unit_tests/models/misc/knapsack.rs`** + +```rust +use crate::models::misc::Knapsack; +use crate::solvers::BruteForce; +use crate::traits::{OptimizationProblem, Problem, Solver}; +use crate::types::{Direction, SolutionSize}; + +#[test] +fn test_knapsack_basic() { + let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); + assert_eq!(problem.num_items(), 4); + assert_eq!(problem.weights(), &[2, 3, 4, 5]); + assert_eq!(problem.values(), &[3, 4, 5, 7]); + assert_eq!(problem.capacity(), 7); + assert_eq!(problem.dims(), vec![2; 4]); + assert_eq!(problem.direction(), Direction::Maximize); + assert_eq!(::NAME, "Knapsack"); + assert_eq!(::variant(), vec![]); +} + +#[test] +fn test_knapsack_evaluate_optimal() { + // Items: w=[2,3,4,5], v=[3,4,5,7], C=7 + // Select items 0 and 3: weight=2+5=7, value=3+7=10 + let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); + assert_eq!(problem.evaluate(&[1, 0, 0, 1]), SolutionSize::Valid(10)); +} + +#[test] +fn test_knapsack_evaluate_feasible() { + let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); + // Select items 0 and 1: weight=2+3=5, value=3+4=7 + assert_eq!(problem.evaluate(&[1, 1, 0, 0]), SolutionSize::Valid(7)); +} + +#[test] +fn test_knapsack_evaluate_overweight() { + let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); + // Select items 2 and 3: weight=4+5=9 > 7 + assert_eq!(problem.evaluate(&[0, 0, 1, 1]), SolutionSize::Invalid); +} + +#[test] +fn test_knapsack_evaluate_empty() { + let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); + // Select nothing: weight=0, value=0 + assert_eq!(problem.evaluate(&[0, 0, 0, 0]), SolutionSize::Valid(0)); +} + +#[test] +fn test_knapsack_evaluate_all_selected() { + let problem = Knapsack::new(vec![1, 1, 1], vec![10, 20, 30], 5); + // Select all: weight=3 <= 5, value=60 + assert_eq!(problem.evaluate(&[1, 1, 1]), SolutionSize::Valid(60)); +} + +#[test] +fn test_knapsack_evaluate_wrong_config_length() { + let problem = Knapsack::new(vec![2, 3], vec![3, 4], 5); + assert_eq!(problem.evaluate(&[1]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 0, 0]), SolutionSize::Invalid); +} + +#[test] +fn test_knapsack_evaluate_invalid_variable_value() { + let problem = Knapsack::new(vec![2, 3], vec![3, 4], 5); + assert_eq!(problem.evaluate(&[2, 0]), SolutionSize::Invalid); +} + +#[test] +fn test_knapsack_empty_instance() { + let problem = Knapsack::new(vec![], vec![], 10); + assert_eq!(problem.num_items(), 0); + assert_eq!(problem.dims(), Vec::::new()); + assert_eq!(problem.evaluate(&[]), SolutionSize::Valid(0)); +} + +#[test] +fn test_knapsack_brute_force() { + let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); + let solver = BruteForce::new(); + let solution = solver.find_best(&problem).expect("should find a solution"); + let metric = problem.evaluate(&solution); + assert_eq!(metric, SolutionSize::Valid(10)); +} + +#[test] +fn test_knapsack_serialization() { + let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); + let json = serde_json::to_value(&problem).unwrap(); + let restored: Knapsack = serde_json::from_value(json).unwrap(); + assert_eq!(restored.weights(), problem.weights()); + assert_eq!(restored.values(), problem.values()); + assert_eq!(restored.capacity(), problem.capacity()); +} + +#[test] +#[should_panic(expected = "weights and values must have the same length")] +fn test_knapsack_mismatched_lengths() { + Knapsack::new(vec![1, 2], vec![3], 5); +} +``` + +**Step 2: Register test module in `src/unit_tests/models/misc/mod.rs`** + +Add `mod knapsack;` if the mod file exists and uses explicit module declarations. + +**Step 3: Run tests** + +Run: `make test` +Expected: All tests pass + +**Step 4: Commit** + +```bash +git add src/unit_tests/models/misc/knapsack.rs src/unit_tests/models/misc/mod.rs +git commit -m "test: add Knapsack unit tests" +``` + +--- + +### Task 3: Register Knapsack in CLI dispatch + +**Files:** +- Modify: `problemreductions-cli/src/dispatch.rs` +- Modify: `problemreductions-cli/src/problem_name.rs` + +**Step 1: Add import in `dispatch.rs`** + +Add `use problemreductions::models::misc::Knapsack;` to the imports. + +**Step 2: Add to `load_problem()` match** + +```rust +"Knapsack" => deser_opt::(data), +``` + +**Step 3: Add to `serialize_any_problem()` match** + +```rust +"Knapsack" => try_ser::(any), +``` + +**Step 4: Add CLI alias in `problem_name.rs`** + +Add to the `ALIASES` array: +```rust +("KS", "Knapsack"), +``` + +Add to `resolve_alias()` match: +```rust +"ks" | "knapsack" => "Knapsack".to_string(), +``` + +**Step 5: Run CLI tests** + +Run: `make cli && make cli-demo` +Expected: SUCCESS + +**Step 6: Commit** + +```bash +git add problemreductions-cli/src/dispatch.rs problemreductions-cli/src/problem_name.rs +git commit -m "feat: register Knapsack in CLI dispatch and aliases" +``` + +--- + +### Task 4: Add Knapsack to the paper + +**Files:** +- Modify: `docs/paper/reductions.typ` + +**Step 1: Add display name** + +In the `display-name` dictionary, add: +```typst +"Knapsack": [Knapsack], +``` + +**Step 2: Add problem definition** + +Use `#problem-def("Knapsack")[...]` with the formal definition from the issue. Include: +- Mathematical formulation (items, weights, values, capacity) +- Configuration variables (binary selection) +- Objective and constraint +- Background on Karp's 21 NP-complete problems, pseudo-polynomial DP, FPTAS + +**Step 3: Verify paper builds** + +Run: `make paper` (or just `make examples && make export-schemas` if paper build requires full toolchain) + +**Step 4: Commit** + +```bash +git add docs/paper/reductions.typ +git commit -m "docs: add Knapsack problem definition to paper" +``` + +--- + +### Task 5: Run full verification + +**Step 1: Run formatting and linting** + +Run: `make fmt && make clippy` + +**Step 2: Run all tests** + +Run: `make test` + +**Step 3: Export schemas** + +Run: `make export-schemas` + +**Step 4: Run check** + +Run: `make check` +Expected: All pass + +**Step 5: Commit any generated files** + +```bash +git add -A +git commit -m "chore: regenerate schemas after Knapsack addition" +``` From 0a8d61cfaec75812f53eb40538de0026a505ae84 Mon Sep 17 00:00:00 2001 From: zazabap Date: Wed, 4 Mar 2026 19:11:29 +0000 Subject: [PATCH 2/9] feat: add Knapsack model with unit tests Co-Authored-By: Claude Opus 4.6 --- src/models/misc/knapsack.rs | 143 +++++++++++++++++++++++++ src/models/misc/mod.rs | 3 + src/unit_tests/models/misc/knapsack.rs | 93 ++++++++++++++++ 3 files changed, 239 insertions(+) create mode 100644 src/models/misc/knapsack.rs create mode 100644 src/unit_tests/models/misc/knapsack.rs diff --git a/src/models/misc/knapsack.rs b/src/models/misc/knapsack.rs new file mode 100644 index 000000000..49e3e90dd --- /dev/null +++ b/src/models/misc/knapsack.rs @@ -0,0 +1,143 @@ +//! Knapsack problem implementation. +//! +//! The 0-1 Knapsack problem asks for a subset of items that maximizes +//! total value while respecting a weight capacity constraint. + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::traits::{OptimizationProblem, Problem}; +use crate::types::{Direction, SolutionSize}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "Knapsack", + module_path: module_path!(), + description: "Select items to maximize total value subject to weight capacity constraint", + fields: &[ + FieldInfo { name: "weights", type_name: "Vec", description: "Item weights w_i" }, + FieldInfo { name: "values", type_name: "Vec", description: "Item values v_i" }, + FieldInfo { name: "capacity", type_name: "i64", description: "Knapsack capacity C" }, + ], + } +} + +/// The 0-1 Knapsack problem. +/// +/// Given `n` items, each with weight `w_i` and value `v_i`, and a capacity `C`, +/// find a subset `S ⊆ {0, ..., n-1}` such that `∑_{i∈S} w_i ≤ C`, +/// maximizing `∑_{i∈S} v_i`. +/// +/// # Representation +/// +/// Each item has a binary variable: `x_i = 1` if item `i` is selected, `0` otherwise. +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::misc::Knapsack; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); +/// let solver = BruteForce::new(); +/// let solution = solver.find_best(&problem); +/// assert!(solution.is_some()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Knapsack { + weights: Vec, + values: Vec, + capacity: i64, +} + +impl Knapsack { + /// Create a new Knapsack instance. + /// + /// # Panics + /// Panics if `weights` and `values` have different lengths. + pub fn new(weights: Vec, values: Vec, capacity: i64) -> Self { + assert_eq!( + weights.len(), + values.len(), + "weights and values must have the same length" + ); + Self { + weights, + values, + capacity, + } + } + + /// Returns the item weights. + pub fn weights(&self) -> &[i64] { + &self.weights + } + + /// Returns the item values. + pub fn values(&self) -> &[i64] { + &self.values + } + + /// Returns the knapsack capacity. + pub fn capacity(&self) -> i64 { + self.capacity + } + + /// Returns the number of items. + pub fn num_items(&self) -> usize { + self.weights.len() + } +} + +impl Problem for Knapsack { + const NAME: &'static str = "Knapsack"; + type Metric = SolutionSize; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + vec![2; self.num_items()] + } + + fn evaluate(&self, config: &[usize]) -> SolutionSize { + if config.len() != self.num_items() { + return SolutionSize::Invalid; + } + if config.iter().any(|&v| v >= 2) { + return SolutionSize::Invalid; + } + let total_weight: i64 = config + .iter() + .enumerate() + .filter(|(_, &x)| x == 1) + .map(|(i, _)| self.weights[i]) + .sum(); + if total_weight > self.capacity { + return SolutionSize::Invalid; + } + let total_value: i64 = config + .iter() + .enumerate() + .filter(|(_, &x)| x == 1) + .map(|(i, _)| self.values[i]) + .sum(); + SolutionSize::Valid(total_value) + } +} + +impl OptimizationProblem for Knapsack { + type Value = i64; + + fn direction(&self) -> Direction { + Direction::Maximize + } +} + +crate::declare_variants! { + Knapsack => "2^(num_items / 2)", +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/knapsack.rs"] +mod tests; diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index 6e2fa084a..cdb66e969 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -3,12 +3,15 @@ //! Problems with unique input structures that don't fit other categories: //! - [`BinPacking`]: Bin Packing (minimize bins) //! - [`Factoring`]: Integer factorization +//! - [`Knapsack`]: 0-1 Knapsack (maximize value subject to weight capacity) //! - [`PaintShop`]: Minimize color switches in paint shop scheduling mod bin_packing; pub(crate) mod factoring; +mod knapsack; pub(crate) mod paintshop; pub use bin_packing::BinPacking; pub use factoring::Factoring; +pub use knapsack::Knapsack; pub use paintshop::PaintShop; diff --git a/src/unit_tests/models/misc/knapsack.rs b/src/unit_tests/models/misc/knapsack.rs new file mode 100644 index 000000000..1f0d83698 --- /dev/null +++ b/src/unit_tests/models/misc/knapsack.rs @@ -0,0 +1,93 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::traits::{OptimizationProblem, Problem}; +use crate::types::Direction; + +#[test] +fn test_knapsack_basic() { + let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); + assert_eq!(problem.num_items(), 4); + assert_eq!(problem.weights(), &[2, 3, 4, 5]); + assert_eq!(problem.values(), &[3, 4, 5, 7]); + assert_eq!(problem.capacity(), 7); + assert_eq!(problem.dims(), vec![2; 4]); + assert_eq!(problem.direction(), Direction::Maximize); + assert_eq!(::NAME, "Knapsack"); + assert_eq!(::variant(), vec![]); +} + +#[test] +fn test_knapsack_evaluate_optimal() { + let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); + assert_eq!(problem.evaluate(&[1, 0, 0, 1]), SolutionSize::Valid(10)); +} + +#[test] +fn test_knapsack_evaluate_feasible() { + let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); + assert_eq!(problem.evaluate(&[1, 1, 0, 0]), SolutionSize::Valid(7)); +} + +#[test] +fn test_knapsack_evaluate_overweight() { + let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); + assert_eq!(problem.evaluate(&[0, 0, 1, 1]), SolutionSize::Invalid); +} + +#[test] +fn test_knapsack_evaluate_empty() { + let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); + assert_eq!(problem.evaluate(&[0, 0, 0, 0]), SolutionSize::Valid(0)); +} + +#[test] +fn test_knapsack_evaluate_all_selected() { + let problem = Knapsack::new(vec![1, 1, 1], vec![10, 20, 30], 5); + assert_eq!(problem.evaluate(&[1, 1, 1]), SolutionSize::Valid(60)); +} + +#[test] +fn test_knapsack_evaluate_wrong_config_length() { + let problem = Knapsack::new(vec![2, 3], vec![3, 4], 5); + assert_eq!(problem.evaluate(&[1]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 0, 0]), SolutionSize::Invalid); +} + +#[test] +fn test_knapsack_evaluate_invalid_variable_value() { + let problem = Knapsack::new(vec![2, 3], vec![3, 4], 5); + assert_eq!(problem.evaluate(&[2, 0]), SolutionSize::Invalid); +} + +#[test] +fn test_knapsack_empty_instance() { + let problem = Knapsack::new(vec![], vec![], 10); + assert_eq!(problem.num_items(), 0); + assert_eq!(problem.dims(), Vec::::new()); + assert_eq!(problem.evaluate(&[]), SolutionSize::Valid(0)); +} + +#[test] +fn test_knapsack_brute_force() { + let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); + let solver = BruteForce::new(); + let solution = solver.find_best(&problem).expect("should find a solution"); + let metric = problem.evaluate(&solution); + assert_eq!(metric, SolutionSize::Valid(10)); +} + +#[test] +fn test_knapsack_serialization() { + let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); + let json = serde_json::to_value(&problem).unwrap(); + let restored: Knapsack = serde_json::from_value(json).unwrap(); + assert_eq!(restored.weights(), problem.weights()); + assert_eq!(restored.values(), problem.values()); + assert_eq!(restored.capacity(), problem.capacity()); +} + +#[test] +#[should_panic(expected = "weights and values must have the same length")] +fn test_knapsack_mismatched_lengths() { + Knapsack::new(vec![1, 2], vec![3], 5); +} From 8da012e9c63ced4912792e5a06e19d8794207ea8 Mon Sep 17 00:00:00 2001 From: zazabap Date: Wed, 4 Mar 2026 19:13:37 +0000 Subject: [PATCH 3/9] feat: register Knapsack in CLI dispatch and aliases Co-Authored-By: Claude Opus 4.6 --- problemreductions-cli/src/dispatch.rs | 4 +++- problemreductions-cli/src/problem_name.rs | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index a6b0011ff..7a8498421 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -1,6 +1,6 @@ use anyhow::{bail, Context, Result}; use problemreductions::models::algebraic::{ClosestVectorProblem, ILP}; -use problemreductions::models::misc::BinPacking; +use problemreductions::models::misc::{BinPacking, Knapsack}; use problemreductions::prelude::*; use problemreductions::rules::{MinimizeSteps, ReductionGraph}; use problemreductions::solvers::{BruteForce, ILPSolver, Solver}; @@ -244,6 +244,7 @@ pub fn load_problem( Some("f64") => deser_opt::>(data), _ => deser_opt::>(data), }, + "Knapsack" => deser_opt::(data), _ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)), } } @@ -303,6 +304,7 @@ pub fn serialize_any_problem( Some("f64") => try_ser::>(any), _ => try_ser::>(any), }, + "Knapsack" => 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 43a5f2c42..6fe6b4819 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -21,6 +21,7 @@ pub const ALIASES: &[(&str, &str)] = &[ ("TSP", "TravelingSalesman"), ("BP", "BinPacking"), ("CVP", "ClosestVectorProblem"), + ("KS", "Knapsack"), ]; /// Resolve a short alias to the canonical problem name. @@ -51,6 +52,7 @@ pub fn resolve_alias(input: &str) -> String { "bicliquecover" => "BicliqueCover".to_string(), "bp" | "binpacking" => "BinPacking".to_string(), "cvp" | "closestvectorproblem" => "ClosestVectorProblem".to_string(), + "ks" | "knapsack" => "Knapsack".to_string(), _ => input.to_string(), // pass-through for exact names } } From d843c3876084a8537084e249519f580f932b9247 Mon Sep 17 00:00:00 2001 From: zazabap Date: Wed, 4 Mar 2026 19:15:43 +0000 Subject: [PATCH 4/9] docs: add Knapsack problem definition to paper Co-Authored-By: Claude Opus 4.6 --- docs/paper/reductions.typ | 9 +++++++++ docs/paper/references.bib | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index d119290fd..c49e158fd 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -39,6 +39,7 @@ "SpinGlass": [Spin Glass], "QUBO": [QUBO], "ILP": [Integer Linear Programming], + "Knapsack": [Knapsack], "Satisfiability": [SAT], "KSatisfiability": [$k$-SAT], "CircuitSAT": [CircuitSAT], @@ -886,6 +887,14 @@ Biclique Cover is equivalent to factoring the biadjacency matrix $M$ of the bipa ) ] +#problem-def("Knapsack")[ + Given $n$ items with weights $w_1, dots, w_n in NN$ and values $v_1, dots, v_n in NN$, and a capacity $C in NN$, find $S subset.eq {0, dots, n - 1}$ maximizing $sum_(i in S) v_i$ subject to $sum_(i in S) w_i lt.eq C$. +][ + One of Karp's 21 NP-complete problems @karp1972. Knapsack is only _weakly_ NP-hard: a classical dynamic-programming algorithm runs in $O(n C)$ pseudo-polynomial time, and a fully polynomial-time approximation scheme (FPTAS) achieves $(1 - epsilon)$-optimal value in $O(n^2 slash epsilon)$ time @ibarra1975. The special case $v_i = w_i$ for all $i$ is the Subset Sum problem. Knapsack is also a special case of Integer Linear Programming with a single constraint. The best known exact algorithm is the $O^*(2^(n slash 2))$ meet-in-the-middle approach of Horowitz and Sahni @horowitz1974, which partitions items into two halves and combines sorted sublists. + + *Example.* Let $n = 4$ items with weights $(2, 3, 4, 5)$, values $(3, 4, 5, 7)$, and capacity $C = 7$. Selecting $S = {1, 2}$ (items with weights 3 and 4) gives total weight $3 + 4 = 7 lt.eq C$ and total value $4 + 5 = 9$. Selecting $S = {0, 3}$ (weights 2 and 5) gives weight $2 + 5 = 7 lt.eq C$ and value $3 + 7 = 10$, which is optimal. +] + // Completeness check: warn about problem types in JSON but missing from paper #{ let json-models = { diff --git a/docs/paper/references.bib b/docs/paper/references.bib index e75278765..c3129a691 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -366,3 +366,24 @@ @article{alber2004 doi = {10.1016/j.jalgor.2003.10.001} } +@article{horowitz1974, + author = {Ellis Horowitz and Sartaj Sahni}, + title = {Computing Partitions with Applications to the Knapsack Problem}, + journal = {Journal of the ACM}, + volume = {21}, + number = {2}, + pages = {277--292}, + year = {1974}, + doi = {10.1145/321812.321823} +} + +@article{ibarra1975, + author = {Oscar H. Ibarra and Chul E. Kim}, + title = {Fast Approximation Algorithms for the Knapsack and Sum of Subset Problems}, + journal = {Journal of the ACM}, + volume = {22}, + number = {4}, + pages = {463--468}, + year = {1975}, + doi = {10.1145/321906.321909} +} From c189700d1edb87f4bfea7c8232e356b99e90f0e2 Mon Sep 17 00:00:00 2001 From: zazabap Date: Wed, 4 Mar 2026 19:20:43 +0000 Subject: [PATCH 5/9] chore: regenerate schemas after Knapsack addition Co-Authored-By: Claude Opus 4.6 --- docs/src/reductions/problem_schemas.json | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index 8cc8d2ae9..02409b123 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -183,6 +183,27 @@ } ] }, + { + "name": "Knapsack", + "description": "Select items to maximize total value subject to weight capacity constraint", + "fields": [ + { + "name": "weights", + "type_name": "Vec", + "description": "Item weights w_i" + }, + { + "name": "values", + "type_name": "Vec", + "description": "Item values v_i" + }, + { + "name": "capacity", + "type_name": "i64", + "description": "Knapsack capacity C" + } + ] + }, { "name": "MaxCut", "description": "Find maximum weight cut in a graph", From 3d0898ca51e165be631686f8c7e618c585074c85 Mon Sep 17 00:00:00 2001 From: zazabap Date: Wed, 4 Mar 2026 19:27:31 +0000 Subject: [PATCH 6/9] fix: add Knapsack re-export and edge-case tests - Re-export Knapsack from models/mod.rs (structural review finding) - Add tests: zero capacity, single item, greedy-not-optimal adversarial case Co-Authored-By: Claude Opus 4.6 --- src/models/mod.rs | 2 +- src/unit_tests/models/misc/knapsack.rs | 35 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/models/mod.rs b/src/models/mod.rs index 15df5cfa3..96b4b79d1 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -15,5 +15,5 @@ pub use graph::{ BicliqueCover, KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumVertexCover, SpinGlass, TravelingSalesman, }; -pub use misc::{BinPacking, Factoring, PaintShop}; +pub use misc::{BinPacking, Factoring, Knapsack, PaintShop}; pub use set::{MaximumSetPacking, MinimumSetCovering}; diff --git a/src/unit_tests/models/misc/knapsack.rs b/src/unit_tests/models/misc/knapsack.rs index 1f0d83698..b658a1a95 100644 --- a/src/unit_tests/models/misc/knapsack.rs +++ b/src/unit_tests/models/misc/knapsack.rs @@ -86,6 +86,41 @@ fn test_knapsack_serialization() { assert_eq!(restored.capacity(), problem.capacity()); } +#[test] +fn test_knapsack_zero_capacity() { + // Capacity 0: only empty set is feasible + let problem = Knapsack::new(vec![1, 2], vec![10, 20], 0); + assert_eq!(problem.evaluate(&[0, 0]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[1, 0]), SolutionSize::Invalid); + let solver = BruteForce::new(); + let solution = solver.find_best(&problem).unwrap(); + assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(0)); +} + +#[test] +fn test_knapsack_single_item() { + // Single item that fits + let problem = Knapsack::new(vec![3], vec![5], 3); + assert_eq!(problem.evaluate(&[1]), SolutionSize::Valid(5)); + assert_eq!(problem.evaluate(&[0]), SolutionSize::Valid(0)); + let solver = BruteForce::new(); + let solution = solver.find_best(&problem).unwrap(); + assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(5)); +} + +#[test] +fn test_knapsack_greedy_not_optimal() { + // Classic case where greedy by value/weight ratio is suboptimal: + // Item 0: w=6, v=7, ratio=1.17 (greedy picks this first, then nothing else fits) + // Item 1: w=5, v=5, ratio=1.00 + // Item 2: w=5, v=5, ratio=1.00 + // Capacity=10. Greedy: {0} value=7. Optimal: {1,2} value=10. + let problem = Knapsack::new(vec![6, 5, 5], vec![7, 5, 5], 10); + let solver = BruteForce::new(); + let solution = solver.find_best(&problem).unwrap(); + assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(10)); +} + #[test] #[should_panic(expected = "weights and values must have the same length")] fn test_knapsack_mismatched_lengths() { From 48239c5bc5991a37cc1161a6e1f9253a82b40836 Mon Sep 17 00:00:00 2001 From: zazabap Date: Sat, 7 Mar 2026 15:17:20 +0000 Subject: [PATCH 7/9] fix: consistent 0-based indexing in paper, add Knapsack to prelude MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Paper definition: use w_0,...,w_(n-1) to match 0-based subset S ⊆ {0,...,n-1} - Add Knapsack to prelude re-export for consistency with other misc models Resolves Copilot review comments on PR #171. Co-Authored-By: Claude Opus 4.6 --- docs/paper/reductions.typ | 2 +- src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index c49e158fd..975d74cb0 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -888,7 +888,7 @@ Biclique Cover is equivalent to factoring the biadjacency matrix $M$ of the bipa ] #problem-def("Knapsack")[ - Given $n$ items with weights $w_1, dots, w_n in NN$ and values $v_1, dots, v_n in NN$, and a capacity $C in NN$, find $S subset.eq {0, dots, n - 1}$ maximizing $sum_(i in S) v_i$ subject to $sum_(i in S) w_i lt.eq C$. + Given $n$ items with weights $w_0, dots, w_(n-1) in NN$ and values $v_0, dots, v_(n-1) in NN$, and a capacity $C in NN$, find $S subset.eq {0, dots, n - 1}$ maximizing $sum_(i in S) v_i$ subject to $sum_(i in S) w_i lt.eq C$. ][ One of Karp's 21 NP-complete problems @karp1972. Knapsack is only _weakly_ NP-hard: a classical dynamic-programming algorithm runs in $O(n C)$ pseudo-polynomial time, and a fully polynomial-time approximation scheme (FPTAS) achieves $(1 - epsilon)$-optimal value in $O(n^2 slash epsilon)$ time @ibarra1975. The special case $v_i = w_i$ for all $i$ is the Subset Sum problem. Knapsack is also a special case of Integer Linear Programming with a single constraint. The best known exact algorithm is the $O^*(2^(n slash 2))$ meet-in-the-middle approach of Horowitz and Sahni @horowitz1974, which partitions items into two halves and combines sorted sublists. diff --git a/src/lib.rs b/src/lib.rs index ef67ab53c..c9ada7ef1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,7 +43,7 @@ pub mod prelude { KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumVertexCover, TravelingSalesman, }; - pub use crate::models::misc::{BinPacking, Factoring, PaintShop}; + pub use crate::models::misc::{BinPacking, Factoring, Knapsack, PaintShop}; pub use crate::models::set::{MaximumSetPacking, MinimumSetCovering}; // Core traits From ff649bc9fa716ef888fa175a29bdbb06855f81ca Mon Sep 17 00:00:00 2001 From: zazabap Date: Sun, 8 Mar 2026 06:46:27 +0000 Subject: [PATCH 8/9] fix: remove plan file and KS/BP aliases per review - Delete docs/plans/2026-03-04-knapsack-model.md - Remove KS and BP short aliases; case-insensitive full names still work Co-Authored-By: Claude Opus 4.6 --- docs/plans/2026-03-04-knapsack-model.md | 420 ---------------------- problemreductions-cli/src/problem_name.rs | 6 +- 2 files changed, 2 insertions(+), 424 deletions(-) delete mode 100644 docs/plans/2026-03-04-knapsack-model.md diff --git a/docs/plans/2026-03-04-knapsack-model.md b/docs/plans/2026-03-04-knapsack-model.md deleted file mode 100644 index 26567cace..000000000 --- a/docs/plans/2026-03-04-knapsack-model.md +++ /dev/null @@ -1,420 +0,0 @@ -# Knapsack Model Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Add the Knapsack (0-1) problem model to the codebase as a new `misc/` category model. - -**Architecture:** Knapsack is a maximization problem with binary variables (select/don't select each item). It has no graph or weight type parameters — all fields are concrete `i64`. The struct stores `weights`, `values`, and `capacity`. Feasibility requires total weight ≤ capacity; objective is total value. Solved via existing BruteForce solver. - -**Tech Stack:** Rust, serde, inventory registration, `Problem`/`OptimizationProblem` traits. - -**Reference:** Follow `add-model` skill Steps 1–7. Reference files: `src/models/misc/bin_packing.rs`, `src/unit_tests/models/misc/bin_packing.rs`. - ---- - -### Task 1: Create the Knapsack model file - -**Files:** -- Create: `src/models/misc/knapsack.rs` -- Modify: `src/models/misc/mod.rs` - -**Step 1: Create `src/models/misc/knapsack.rs`** - -```rust -//! Knapsack problem implementation. -//! -//! The 0-1 Knapsack problem asks for a subset of items that maximizes -//! total value while respecting a weight capacity constraint. - -use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; -use serde::{Deserialize, Serialize}; - -inventory::submit! { - ProblemSchemaEntry { - name: "Knapsack", - module_path: module_path!(), - description: "Select items to maximize total value subject to weight capacity constraint", - fields: &[ - FieldInfo { name: "weights", type_name: "Vec", description: "Item weights w_i" }, - FieldInfo { name: "values", type_name: "Vec", description: "Item values v_i" }, - FieldInfo { name: "capacity", type_name: "i64", description: "Knapsack capacity C" }, - ], - } -} - -/// The 0-1 Knapsack problem. -/// -/// Given `n` items, each with weight `w_i` and value `v_i`, and a capacity `C`, -/// find a subset `S ⊆ {0, ..., n-1}` such that `∑_{i∈S} w_i ≤ C`, -/// maximizing `∑_{i∈S} v_i`. -/// -/// # Representation -/// -/// Each item has a binary variable: `x_i = 1` if item `i` is selected, `0` otherwise. -/// -/// # Example -/// -/// ``` -/// use problemreductions::models::misc::Knapsack; -/// use problemreductions::{Problem, Solver, BruteForce}; -/// -/// let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); -/// let solver = BruteForce::new(); -/// let solution = solver.find_best(&problem); -/// assert!(solution.is_some()); -/// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Knapsack { - weights: Vec, - values: Vec, - capacity: i64, -} - -impl Knapsack { - /// Create a new Knapsack instance. - /// - /// # Panics - /// Panics if `weights` and `values` have different lengths. - pub fn new(weights: Vec, values: Vec, capacity: i64) -> Self { - assert_eq!( - weights.len(), - values.len(), - "weights and values must have the same length" - ); - Self { - weights, - values, - capacity, - } - } - - /// Returns the item weights. - pub fn weights(&self) -> &[i64] { - &self.weights - } - - /// Returns the item values. - pub fn values(&self) -> &[i64] { - &self.values - } - - /// Returns the knapsack capacity. - pub fn capacity(&self) -> i64 { - self.capacity - } - - /// Returns the number of items. - pub fn num_items(&self) -> usize { - self.weights.len() - } -} - -impl Problem for Knapsack { - const NAME: &'static str = "Knapsack"; - type Metric = SolutionSize; - - fn variant() -> Vec<(&'static str, &'static str)> { - crate::variant_params![] - } - - fn dims(&self) -> Vec { - vec![2; self.num_items()] - } - - fn evaluate(&self, config: &[usize]) -> SolutionSize { - if config.len() != self.num_items() { - return SolutionSize::Invalid; - } - if config.iter().any(|&v| v >= 2) { - return SolutionSize::Invalid; - } - let total_weight: i64 = config - .iter() - .enumerate() - .filter(|(_, &x)| x == 1) - .map(|(i, _)| self.weights[i]) - .sum(); - if total_weight > self.capacity { - return SolutionSize::Invalid; - } - let total_value: i64 = config - .iter() - .enumerate() - .filter(|(_, &x)| x == 1) - .map(|(i, _)| self.values[i]) - .sum(); - SolutionSize::Valid(total_value) - } -} - -impl OptimizationProblem for Knapsack { - type Value = i64; - - fn direction(&self) -> Direction { - Direction::Maximize - } -} - -crate::declare_variants! { - Knapsack => "2^(num_items / 2)", -} - -#[cfg(test)] -#[path = "../../unit_tests/models/misc/knapsack.rs"] -mod tests; -``` - -**Step 2: Register in `src/models/misc/mod.rs`** - -Add `mod knapsack;` and `pub use knapsack::Knapsack;` following the existing pattern (alphabetical order). - -**Step 3: Verify it compiles** - -Run: `make build` -Expected: SUCCESS - -**Step 4: Commit** - -```bash -git add src/models/misc/knapsack.rs src/models/misc/mod.rs -git commit -m "feat: add Knapsack model struct and Problem impl" -``` - ---- - -### Task 2: Write unit tests for Knapsack - -**Files:** -- Create: `src/unit_tests/models/misc/knapsack.rs` -- Modify: `src/unit_tests/models/misc/mod.rs` (if it exists) - -**Step 1: Create `src/unit_tests/models/misc/knapsack.rs`** - -```rust -use crate::models::misc::Knapsack; -use crate::solvers::BruteForce; -use crate::traits::{OptimizationProblem, Problem, Solver}; -use crate::types::{Direction, SolutionSize}; - -#[test] -fn test_knapsack_basic() { - let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); - assert_eq!(problem.num_items(), 4); - assert_eq!(problem.weights(), &[2, 3, 4, 5]); - assert_eq!(problem.values(), &[3, 4, 5, 7]); - assert_eq!(problem.capacity(), 7); - assert_eq!(problem.dims(), vec![2; 4]); - assert_eq!(problem.direction(), Direction::Maximize); - assert_eq!(::NAME, "Knapsack"); - assert_eq!(::variant(), vec![]); -} - -#[test] -fn test_knapsack_evaluate_optimal() { - // Items: w=[2,3,4,5], v=[3,4,5,7], C=7 - // Select items 0 and 3: weight=2+5=7, value=3+7=10 - let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); - assert_eq!(problem.evaluate(&[1, 0, 0, 1]), SolutionSize::Valid(10)); -} - -#[test] -fn test_knapsack_evaluate_feasible() { - let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); - // Select items 0 and 1: weight=2+3=5, value=3+4=7 - assert_eq!(problem.evaluate(&[1, 1, 0, 0]), SolutionSize::Valid(7)); -} - -#[test] -fn test_knapsack_evaluate_overweight() { - let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); - // Select items 2 and 3: weight=4+5=9 > 7 - assert_eq!(problem.evaluate(&[0, 0, 1, 1]), SolutionSize::Invalid); -} - -#[test] -fn test_knapsack_evaluate_empty() { - let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); - // Select nothing: weight=0, value=0 - assert_eq!(problem.evaluate(&[0, 0, 0, 0]), SolutionSize::Valid(0)); -} - -#[test] -fn test_knapsack_evaluate_all_selected() { - let problem = Knapsack::new(vec![1, 1, 1], vec![10, 20, 30], 5); - // Select all: weight=3 <= 5, value=60 - assert_eq!(problem.evaluate(&[1, 1, 1]), SolutionSize::Valid(60)); -} - -#[test] -fn test_knapsack_evaluate_wrong_config_length() { - let problem = Knapsack::new(vec![2, 3], vec![3, 4], 5); - assert_eq!(problem.evaluate(&[1]), SolutionSize::Invalid); - assert_eq!(problem.evaluate(&[1, 0, 0]), SolutionSize::Invalid); -} - -#[test] -fn test_knapsack_evaluate_invalid_variable_value() { - let problem = Knapsack::new(vec![2, 3], vec![3, 4], 5); - assert_eq!(problem.evaluate(&[2, 0]), SolutionSize::Invalid); -} - -#[test] -fn test_knapsack_empty_instance() { - let problem = Knapsack::new(vec![], vec![], 10); - assert_eq!(problem.num_items(), 0); - assert_eq!(problem.dims(), Vec::::new()); - assert_eq!(problem.evaluate(&[]), SolutionSize::Valid(0)); -} - -#[test] -fn test_knapsack_brute_force() { - let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); - let solver = BruteForce::new(); - let solution = solver.find_best(&problem).expect("should find a solution"); - let metric = problem.evaluate(&solution); - assert_eq!(metric, SolutionSize::Valid(10)); -} - -#[test] -fn test_knapsack_serialization() { - let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); - let json = serde_json::to_value(&problem).unwrap(); - let restored: Knapsack = serde_json::from_value(json).unwrap(); - assert_eq!(restored.weights(), problem.weights()); - assert_eq!(restored.values(), problem.values()); - assert_eq!(restored.capacity(), problem.capacity()); -} - -#[test] -#[should_panic(expected = "weights and values must have the same length")] -fn test_knapsack_mismatched_lengths() { - Knapsack::new(vec![1, 2], vec![3], 5); -} -``` - -**Step 2: Register test module in `src/unit_tests/models/misc/mod.rs`** - -Add `mod knapsack;` if the mod file exists and uses explicit module declarations. - -**Step 3: Run tests** - -Run: `make test` -Expected: All tests pass - -**Step 4: Commit** - -```bash -git add src/unit_tests/models/misc/knapsack.rs src/unit_tests/models/misc/mod.rs -git commit -m "test: add Knapsack unit tests" -``` - ---- - -### Task 3: Register Knapsack in CLI dispatch - -**Files:** -- Modify: `problemreductions-cli/src/dispatch.rs` -- Modify: `problemreductions-cli/src/problem_name.rs` - -**Step 1: Add import in `dispatch.rs`** - -Add `use problemreductions::models::misc::Knapsack;` to the imports. - -**Step 2: Add to `load_problem()` match** - -```rust -"Knapsack" => deser_opt::(data), -``` - -**Step 3: Add to `serialize_any_problem()` match** - -```rust -"Knapsack" => try_ser::(any), -``` - -**Step 4: Add CLI alias in `problem_name.rs`** - -Add to the `ALIASES` array: -```rust -("KS", "Knapsack"), -``` - -Add to `resolve_alias()` match: -```rust -"ks" | "knapsack" => "Knapsack".to_string(), -``` - -**Step 5: Run CLI tests** - -Run: `make cli && make cli-demo` -Expected: SUCCESS - -**Step 6: Commit** - -```bash -git add problemreductions-cli/src/dispatch.rs problemreductions-cli/src/problem_name.rs -git commit -m "feat: register Knapsack in CLI dispatch and aliases" -``` - ---- - -### Task 4: Add Knapsack to the paper - -**Files:** -- Modify: `docs/paper/reductions.typ` - -**Step 1: Add display name** - -In the `display-name` dictionary, add: -```typst -"Knapsack": [Knapsack], -``` - -**Step 2: Add problem definition** - -Use `#problem-def("Knapsack")[...]` with the formal definition from the issue. Include: -- Mathematical formulation (items, weights, values, capacity) -- Configuration variables (binary selection) -- Objective and constraint -- Background on Karp's 21 NP-complete problems, pseudo-polynomial DP, FPTAS - -**Step 3: Verify paper builds** - -Run: `make paper` (or just `make examples && make export-schemas` if paper build requires full toolchain) - -**Step 4: Commit** - -```bash -git add docs/paper/reductions.typ -git commit -m "docs: add Knapsack problem definition to paper" -``` - ---- - -### Task 5: Run full verification - -**Step 1: Run formatting and linting** - -Run: `make fmt && make clippy` - -**Step 2: Run all tests** - -Run: `make test` - -**Step 3: Export schemas** - -Run: `make export-schemas` - -**Step 4: Run check** - -Run: `make check` -Expected: All pass - -**Step 5: Commit any generated files** - -```bash -git add -A -git commit -m "chore: regenerate schemas after Knapsack addition" -``` diff --git a/problemreductions-cli/src/problem_name.rs b/problemreductions-cli/src/problem_name.rs index 4a6cefade..acd9b4b59 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -19,9 +19,7 @@ pub const ALIASES: &[(&str, &str)] = &[ ("3SAT", "KSatisfiability"), ("KSAT", "KSatisfiability"), ("TSP", "TravelingSalesman"), - ("BP", "BinPacking"), ("CVP", "ClosestVectorProblem"), - ("KS", "Knapsack"), ("MaxMatching", "MaximumMatching"), ]; @@ -51,9 +49,9 @@ pub fn resolve_alias(input: &str) -> String { "paintshop" => "PaintShop".to_string(), "bmf" => "BMF".to_string(), "bicliquecover" => "BicliqueCover".to_string(), - "bp" | "binpacking" => "BinPacking".to_string(), + "binpacking" => "BinPacking".to_string(), "cvp" | "closestvectorproblem" => "ClosestVectorProblem".to_string(), - "ks" | "knapsack" => "Knapsack".to_string(), + "knapsack" => "Knapsack".to_string(), _ => input.to_string(), // pass-through for exact names } } From 113036074bc23299ab10396410df07d1fd1a97fc Mon Sep 17 00:00:00 2001 From: zazabap Date: Sun, 8 Mar 2026 08:32:13 +0000 Subject: [PATCH 9/9] fix: regenerate reduction_graph.json and problem_schemas.json to include Knapsack The Knapsack node was missing from the exported graph and schemas, which would cause the paper's completeness check to fail. Co-Authored-By: Claude Opus 4.6 --- docs/src/reductions/problem_schemas.json | 34 +---- docs/src/reductions/reduction_graph.json | 171 ++++++++++++----------- 2 files changed, 91 insertions(+), 114 deletions(-) diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index 02409b123..15eafb74c 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -8,16 +8,6 @@ "type_name": "Vec>", "description": "Target boolean matrix A" }, - { - "name": "m", - "type_name": "usize", - "description": "Number of rows" - }, - { - "name": "n", - "type_name": "usize", - "description": "Number of columns" - }, { "name": "k", "type_name": "usize", @@ -75,11 +65,6 @@ "name": "circuit", "type_name": "Circuit", "description": "The boolean circuit" - }, - { - "name": "variables", - "type_name": "Vec", - "description": "Circuit variable names" } ] }, @@ -358,24 +343,9 @@ "description": "Minimize color changes in paint shop sequence", "fields": [ { - "name": "sequence_indices", - "type_name": "Vec", - "description": "Car sequence as indices" - }, - { - "name": "car_labels", + "name": "sequence", "type_name": "Vec", - "description": "Unique car labels" - }, - { - "name": "is_first", - "type_name": "Vec", - "description": "First occurrence flags" - }, - { - "name": "num_cars", - "type_name": "usize", - "description": "Number of unique cars" + "description": "Car labels (each must appear exactly twice)" } ] }, diff --git a/docs/src/reductions/reduction_graph.json b/docs/src/reductions/reduction_graph.json index 0b0c68506..3e73b9c15 100644 --- a/docs/src/reductions/reduction_graph.json +++ b/docs/src/reductions/reduction_graph.json @@ -148,6 +148,13 @@ "doc_path": "models/formula/struct.KSatisfiability.html", "complexity": "2^num_variables" }, + { + "name": "Knapsack", + "variant": {}, + "category": "misc", + "doc_path": "models/misc/struct.Knapsack.html", + "complexity": "2^(num_items / 2)" + }, { "name": "MaxCut", "variant": { @@ -386,7 +393,7 @@ }, { "source": 4, - "target": 38, + "target": 39, "overhead": [ { "field": "num_spins", @@ -431,7 +438,7 @@ }, { "source": 8, - "target": 35, + "target": 36, "overhead": [ { "field": "num_vars", @@ -472,7 +479,7 @@ }, { "source": 13, - "target": 35, + "target": 36, "overhead": [ { "field": "num_vars", @@ -498,7 +505,7 @@ }, { "source": 14, - "target": 35, + "target": 36, "overhead": [ { "field": "num_vars", @@ -509,7 +516,7 @@ }, { "source": 14, - "target": 36, + "target": 37, "overhead": [ { "field": "num_clauses", @@ -543,7 +550,7 @@ }, { "source": 15, - "target": 35, + "target": 36, "overhead": [ { "field": "num_vars", @@ -554,7 +561,7 @@ }, { "source": 15, - "target": 36, + "target": 37, "overhead": [ { "field": "num_clauses", @@ -573,7 +580,7 @@ }, { "source": 16, - "target": 36, + "target": 37, "overhead": [ { "field": "num_clauses", @@ -591,8 +598,8 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 17, - "target": 38, + "source": 18, + "target": 39, "overhead": [ { "field": "num_spins", @@ -606,7 +613,7 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 19, + "source": 20, "target": 8, "overhead": [ { @@ -621,8 +628,8 @@ "doc_path": "rules/maximumclique_ilp/index.html" }, { - "source": 20, - "target": 21, + "source": 21, + "target": 22, "overhead": [ { "field": "num_vertices", @@ -636,8 +643,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 20, - "target": 25, + "source": 21, + "target": 26, "overhead": [ { "field": "num_vertices", @@ -651,8 +658,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 21, - "target": 26, + "source": 22, + "target": 27, "overhead": [ { "field": "num_vertices", @@ -666,8 +673,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 22, - "target": 20, + "source": 23, + "target": 21, "overhead": [ { "field": "num_vertices", @@ -681,8 +688,8 @@ "doc_path": "rules/maximumindependentset_gridgraph/index.html" }, { - "source": 22, - "target": 21, + "source": 23, + "target": 22, "overhead": [ { "field": "num_vertices", @@ -696,8 +703,8 @@ "doc_path": "rules/maximumindependentset_gridgraph/index.html" }, { - "source": 22, - "target": 23, + "source": 23, + "target": 24, "overhead": [ { "field": "num_vertices", @@ -711,8 +718,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 22, - "target": 24, + "source": 23, + "target": 25, "overhead": [ { "field": "num_vertices", @@ -726,8 +733,8 @@ "doc_path": "rules/maximumindependentset_triangular/index.html" }, { - "source": 22, - "target": 28, + "source": 23, + "target": 29, "overhead": [ { "field": "num_sets", @@ -741,7 +748,7 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 23, + "source": 24, "target": 8, "overhead": [ { @@ -756,8 +763,8 @@ "doc_path": "rules/maximumindependentset_ilp/index.html" }, { - "source": 23, - "target": 30, + "source": 24, + "target": 31, "overhead": [ { "field": "num_sets", @@ -771,8 +778,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 23, - "target": 33, + "source": 24, + "target": 34, "overhead": [ { "field": "num_vertices", @@ -786,8 +793,8 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 23, - "target": 35, + "source": 24, + "target": 36, "overhead": [ { "field": "num_vars", @@ -797,8 +804,8 @@ "doc_path": "rules/maximumindependentset_qubo/index.html" }, { - "source": 24, - "target": 26, + "source": 25, + "target": 27, "overhead": [ { "field": "num_vertices", @@ -812,8 +819,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 25, - "target": 22, + "source": 26, + "target": 23, "overhead": [ { "field": "num_vertices", @@ -827,8 +834,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 25, - "target": 26, + "source": 26, + "target": 27, "overhead": [ { "field": "num_vertices", @@ -842,8 +849,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 26, - "target": 23, + "source": 27, + "target": 24, "overhead": [ { "field": "num_vertices", @@ -857,7 +864,7 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 27, + "source": 28, "target": 8, "overhead": [ { @@ -872,8 +879,8 @@ "doc_path": "rules/maximummatching_ilp/index.html" }, { - "source": 27, - "target": 30, + "source": 28, + "target": 31, "overhead": [ { "field": "num_sets", @@ -887,8 +894,8 @@ "doc_path": "rules/maximummatching_maximumsetpacking/index.html" }, { - "source": 28, - "target": 22, + "source": 29, + "target": 23, "overhead": [ { "field": "num_vertices", @@ -902,8 +909,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 28, - "target": 30, + "source": 29, + "target": 31, "overhead": [ { "field": "num_sets", @@ -917,8 +924,8 @@ "doc_path": "rules/maximumsetpacking_casts/index.html" }, { - "source": 29, - "target": 35, + "source": 30, + "target": 36, "overhead": [ { "field": "num_vars", @@ -928,7 +935,7 @@ "doc_path": "rules/maximumsetpacking_qubo/index.html" }, { - "source": 30, + "source": 31, "target": 8, "overhead": [ { @@ -943,8 +950,8 @@ "doc_path": "rules/maximumsetpacking_ilp/index.html" }, { - "source": 30, - "target": 23, + "source": 31, + "target": 24, "overhead": [ { "field": "num_vertices", @@ -958,8 +965,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 30, - "target": 29, + "source": 31, + "target": 30, "overhead": [ { "field": "num_sets", @@ -973,7 +980,7 @@ "doc_path": "rules/maximumsetpacking_casts/index.html" }, { - "source": 31, + "source": 32, "target": 8, "overhead": [ { @@ -988,7 +995,7 @@ "doc_path": "rules/minimumdominatingset_ilp/index.html" }, { - "source": 32, + "source": 33, "target": 8, "overhead": [ { @@ -1003,7 +1010,7 @@ "doc_path": "rules/minimumsetcovering_ilp/index.html" }, { - "source": 33, + "source": 34, "target": 8, "overhead": [ { @@ -1018,8 +1025,8 @@ "doc_path": "rules/minimumvertexcover_ilp/index.html" }, { - "source": 33, - "target": 23, + "source": 34, + "target": 24, "overhead": [ { "field": "num_vertices", @@ -1033,8 +1040,8 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 33, - "target": 32, + "source": 34, + "target": 33, "overhead": [ { "field": "num_sets", @@ -1048,8 +1055,8 @@ "doc_path": "rules/minimumvertexcover_minimumsetcovering/index.html" }, { - "source": 33, - "target": 35, + "source": 34, + "target": 36, "overhead": [ { "field": "num_vars", @@ -1059,7 +1066,7 @@ "doc_path": "rules/minimumvertexcover_qubo/index.html" }, { - "source": 35, + "source": 36, "target": 8, "overhead": [ { @@ -1074,8 +1081,8 @@ "doc_path": "rules/qubo_ilp/index.html" }, { - "source": 35, - "target": 37, + "source": 36, + "target": 38, "overhead": [ { "field": "num_spins", @@ -1085,7 +1092,7 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 36, + "source": 37, "target": 4, "overhead": [ { @@ -1100,7 +1107,7 @@ "doc_path": "rules/sat_circuitsat/index.html" }, { - "source": 36, + "source": 37, "target": 10, "overhead": [ { @@ -1115,7 +1122,7 @@ "doc_path": "rules/sat_coloring/index.html" }, { - "source": 36, + "source": 37, "target": 15, "overhead": [ { @@ -1130,8 +1137,8 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 36, - "target": 22, + "source": 37, + "target": 23, "overhead": [ { "field": "num_vertices", @@ -1145,8 +1152,8 @@ "doc_path": "rules/sat_maximumindependentset/index.html" }, { - "source": 36, - "target": 31, + "source": 37, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -1160,8 +1167,8 @@ "doc_path": "rules/sat_minimumdominatingset/index.html" }, { - "source": 37, - "target": 35, + "source": 38, + "target": 36, "overhead": [ { "field": "num_vars", @@ -1171,8 +1178,8 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 38, - "target": 17, + "source": 39, + "target": 18, "overhead": [ { "field": "num_vertices", @@ -1186,8 +1193,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 38, - "target": 37, + "source": 39, + "target": 38, "overhead": [ { "field": "num_spins", @@ -1201,7 +1208,7 @@ "doc_path": "rules/spinglass_casts/index.html" }, { - "source": 39, + "source": 40, "target": 8, "overhead": [ {