Skip to content

Fix #639: [Rule] Knapsack to ILP#688

Merged
isPANN merged 6 commits intomainfrom
issue-639-knapsack-to-ilp
Mar 20, 2026
Merged

Fix #639: [Rule] Knapsack to ILP#688
isPANN merged 6 commits intomainfrom
issue-639-knapsack-to-ilp

Conversation

@GiggleLiu
Copy link
Copy Markdown
Contributor

Summary

Fixes #639

@GiggleLiu
Copy link
Copy Markdown
Contributor Author

Implementation Summary

Changes

  • Added the direct Knapsack -> ILP rule in src/rules/knapsack_ilp.rs, including the canonical issue example and identity solution extraction for the binary ILP formulation.
  • Registered the new rule and updated tests so the reduction graph now expects the direct Knapsack -> ILP edge, while the dominated-rule allow-list now includes the existing Knapsack -> QUBO rule because it is dominated by Knapsack -> ILP -> QUBO.
  • Added the paper entry and citation for the reduction, and regenerated the canonical example fixture so examples.json includes the new Knapsack -> ILP example.
  • Tightened rule tests with an empty-instance case and an exact canonical-example witness assertion.

Deviations from Plan

  • None.

Open Questions

  • None.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements and documents a new direct reduction rule from Knapsack to ILP (binary), connecting Knapsack into the ILP-centered reduction graph and adding corresponding tests and example-db fixtures.

Changes:

  • Add Knapsack -> ILP<bool> reduction rule with canonical example spec registration.
  • Add unit tests validating closed-loop correctness, ILP structure, and reduction-graph reachability; update dominated-rule expectations.
  • Update example-db fixtures and paper docs to include the new Knapsack→ILP canonical example and citation.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/rules/knapsack_ilp.rs New Knapsack→ILP reduction implementation + example-db spec + tests module hook
src/rules/mod.rs Registers the new rule module and its canonical example specs under ilp-solver
src/unit_tests/rules/knapsack_ilp.rs New unit tests for reduction correctness/structure and example-db spec
src/unit_tests/rules/graph.rs Adds a reduction-graph path test asserting direct Knapsack→ILP step exists
src/unit_tests/rules/analysis.rs Updates dominated-rule allow-list to include Knapsack→QUBO dominated via Knapsack→ILP→QUBO
src/example_db/fixtures/examples.json Adds Knapsack→ILP fixture; also includes broader fixture updates
docs/paper/references.bib Adds Papadimitriou & Steiglitz (1982) book reference
docs/paper/reductions.typ Adds paper section describing Knapsack→ILP reduction with a loaded canonical example

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 52 to 60
{"source":{"problem":"KSatisfiability","variant":{"k":"K2"},"instance":{"clauses":[{"literals":[1,2]},{"literals":[-1,3]},{"literals":[-2,4]},{"literals":[-3,-4]}],"num_vars":4}},"target":{"problem":"QUBO","variant":{"weight":"f64"},"instance":{"matrix":[[0.0,1.0,-1.0,0.0],[0.0,0.0,0.0,-1.0],[0.0,0.0,0.0,1.0],[0.0,0.0,0.0,0.0]],"num_vars":4}},"solutions":[{"source_config":[0,1,0,1],"target_config":[0,1,0,1]}]},
{"source":{"problem":"KSatisfiability","variant":{"k":"K3"},"instance":{"clauses":[{"literals":[1,2,-3]},{"literals":[-1,3,4]},{"literals":[2,-4,5]},{"literals":[-2,3,-5]},{"literals":[1,-3,5]},{"literals":[-1,-2,4]},{"literals":[3,-4,-5]}],"num_vars":5}},"target":{"problem":"QUBO","variant":{"weight":"f64"},"instance":{"matrix":[[0.0,4.0,-4.0,0.0,0.0,4.0,-4.0,0.0,0.0,4.0,-4.0,0.0],[0.0,0.0,-2.0,-2.0,0.0,4.0,0.0,4.0,-4.0,0.0,-4.0,0.0],[0.0,0.0,2.0,-2.0,0.0,1.0,4.0,0.0,4.0,-4.0,0.0,4.0],[0.0,0.0,0.0,4.0,0.0,0.0,-1.0,-4.0,0.0,0.0,-1.0,-4.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,-1.0,1.0,-1.0,0.0,1.0],[0.0,0.0,0.0,0.0,0.0,-2.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,3.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,3.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,2.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,3.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,7.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,2.0]],"num_vars":12}},"solutions":[{"source_config":[0,0,0,0,0],"target_config":[0,0,0,0,0,1,0,0,0,0,0,0]}]},
{"source":{"problem":"KSatisfiability","variant":{"k":"K3"},"instance":{"clauses":[{"literals":[1,2,-3]},{"literals":[-1,3,4]},{"literals":[2,-4,5]},{"literals":[-2,3,-5]},{"literals":[1,-3,5]},{"literals":[-1,-2,4]},{"literals":[3,-4,-5]}],"num_vars":5}},"target":{"problem":"QUBO","variant":{"weight":"f64"},"instance":{"matrix":[[0.0,4.0,-4.0,0.0,0.0,4.0,-4.0,0.0,0.0,4.0,-4.0,0.0],[0.0,0.0,-2.0,-2.0,0.0,4.0,0.0,4.0,-4.0,0.0,-4.0,0.0],[0.0,0.0,2.0,-2.0,0.0,1.0,4.0,0.0,4.0,-4.0,0.0,4.0],[0.0,0.0,0.0,4.0,0.0,0.0,-1.0,-4.0,0.0,0.0,-1.0,-4.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,-1.0,1.0,-1.0,0.0,1.0],[0.0,0.0,0.0,0.0,0.0,-2.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,3.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,3.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,2.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,3.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,7.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,2.0]],"num_vars":12}},"solutions":[{"source_config":[1,1,1,1,1],"target_config":[1,1,1,1,1,0,0,0,0,0,1,0]}]},
{"source":{"problem":"KSatisfiability","variant":{"k":"K3"},"instance":{"clauses":[{"literals":[1,2,3]},{"literals":[-1,-2,3]}],"num_vars":3}},"target":{"problem":"SubsetSum","variant":{},"instance":{"sizes":["10010","10001","1010","1001","111","100","10","20","1","2"],"target":"11144"}},"solutions":[{"source_config":[0,0,1],"target_config":[0,1,0,1,1,0,1,1,1,0]}]},
{"source":{"problem":"KSatisfiability","variant":{"k":"KN"},"instance":{"clauses":[{"literals":[1,-2,3]},{"literals":[-1,3,4]},{"literals":[2,-3,-4]}],"num_vars":4}},"target":{"problem":"Satisfiability","variant":{},"instance":{"clauses":[{"literals":[1,-2,3]},{"literals":[-1,3,4]},{"literals":[2,-3,-4]}],"num_vars":4}},"solutions":[{"source_config":[1,1,1,0],"target_config":[1,1,1,0]}]},
{"source":{"problem":"Knapsack","variant":{},"instance":{"capacity":7,"values":[1,4,5,7],"weights":[1,3,4,5]}},"target":{"problem":"ILP","variant":{"variable":"bool"},"instance":{"constraints":[{"cmp":"Le","rhs":7.0,"terms":[[0,1.0],[1,3.0],[2,4.0],[3,5.0]]}],"num_vars":4,"objective":[[0,1.0],[1,4.0],[2,5.0],[3,7.0]],"sense":"Maximize"}},"solutions":[{"source_config":[0,1,1,0],"target_config":[0,1,1,0]}]},
{"source":{"problem":"Knapsack","variant":{},"instance":{"capacity":7,"values":[3,4,5,7],"weights":[2,3,4,5]}},"target":{"problem":"QUBO","variant":{"weight":"f64"},"instance":{"matrix":[[-483.0,240.0,320.0,400.0,80.0,160.0,320.0],[0.0,-664.0,480.0,600.0,120.0,240.0,480.0],[0.0,0.0,-805.0,800.0,160.0,320.0,640.0],[0.0,0.0,0.0,-907.0,200.0,400.0,800.0],[0.0,0.0,0.0,0.0,-260.0,80.0,160.0],[0.0,0.0,0.0,0.0,0.0,-480.0,320.0],[0.0,0.0,0.0,0.0,0.0,0.0,-800.0]],"num_vars":7}},"solutions":[{"source_config":[1,0,0,1],"target_config":[1,0,0,1,0,0,0]}]},
{"source":{"problem":"LongestCommonSubsequence","variant":{},"instance":{"strings":[[65,66,65,67],[66,65,67,65]]}},"target":{"problem":"ILP","variant":{"variable":"bool"},"instance":{"constraints":[{"cmp":"Le","rhs":1.0,"terms":[[0,1.0],[1,1.0]]},{"cmp":"Le","rhs":1.0,"terms":[[2,1.0]]},{"cmp":"Le","rhs":1.0,"terms":[[3,1.0],[4,1.0]]},{"cmp":"Le","rhs":1.0,"terms":[[5,1.0]]},{"cmp":"Le","rhs":1.0,"terms":[[2,1.0]]},{"cmp":"Le","rhs":1.0,"terms":[[0,1.0],[3,1.0]]},{"cmp":"Le","rhs":1.0,"terms":[[5,1.0]]},{"cmp":"Le","rhs":1.0,"terms":[[1,1.0],[4,1.0]]},{"cmp":"Le","rhs":1.0,"terms":[[0,1.0],[2,1.0]]},{"cmp":"Le","rhs":1.0,"terms":[[1,1.0],[2,1.0]]},{"cmp":"Le","rhs":1.0,"terms":[[1,1.0],[3,1.0]]},{"cmp":"Le","rhs":1.0,"terms":[[1,1.0],[5,1.0]]},{"cmp":"Le","rhs":1.0,"terms":[[4,1.0],[5,1.0]]}],"num_vars":6,"objective":[[0,1.0],[1,1.0],[2,1.0],[3,1.0],[4,1.0],[5,1.0]],"sense":"Maximize"}},"solutions":[{"source_config":[0,1,1,1],"target_config":[0,0,1,1,0,1]}]},
{"source":{"problem":"MaxCut","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"edge_weights":[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,4,null],[0,5,null],[1,2,null],[1,6,null],[2,3,null],[2,7,null],[3,4,null],[3,8,null],[4,9,null],[5,7,null],[5,8,null],[6,8,null],[6,9,null],[7,9,null]],"node_holes":[],"nodes":[null,null,null,null,null,null,null,null,null,null]}}}},"target":{"problem":"SpinGlass","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"couplings":[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],"fields":[0,0,0,0,0,0,0,0,0,0],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,4,null],[0,5,null],[1,2,null],[1,6,null],[2,3,null],[2,7,null],[3,4,null],[3,8,null],[4,9,null],[5,7,null],[5,8,null],[6,8,null],[6,9,null],[7,9,null]],"node_holes":[],"nodes":[null,null,null,null,null,null,null,null,null,null]}}}},"solutions":[{"source_config":[0,1,0,1,0,1,0,0,0,1],"target_config":[0,1,0,1,0,1,0,0,0,1]}]},
{"source":{"problem":"MaxCut","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"edge_weights":[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,4,null],[0,5,null],[1,2,null],[1,6,null],[2,3,null],[2,7,null],[3,4,null],[3,8,null],[4,9,null],[5,7,null],[5,8,null],[6,8,null],[6,9,null],[7,9,null]],"node_holes":[],"nodes":[null,null,null,null,null,null,null,null,null,null]}}}},"target":{"problem":"SpinGlass","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"couplings":[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],"fields":[0,0,0,0,0,0,0,0,0,0],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,4,null],[0,5,null],[1,2,null],[1,6,null],[2,3,null],[2,7,null],[3,4,null],[3,8,null],[4,9,null],[5,7,null],[5,8,null],[6,8,null],[6,9,null],[7,9,null]],"node_holes":[],"nodes":[null,null,null,null,null,null,null,null,null,null]}}}},"solutions":[{"source_config":[1,0,1,0,0,0,0,0,1,1],"target_config":[1,0,1,0,0,0,0,0,1,1]}]},
{"source":{"problem":"MaximumClique","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[0,3,null],[0,4,null],[1,2,null],[1,3,null],[1,5,null],[2,4,null],[2,5,null],[3,4,null],[3,5,null],[4,5,null]],"node_holes":[],"nodes":[null,null,null,null,null,null]}},"weights":[1,1,1,1,1,1]}},"target":{"problem":"ILP","variant":{"variable":"bool"},"instance":{"constraints":[{"cmp":"Le","rhs":1.0,"terms":[[0,1.0],[5,1.0]]},{"cmp":"Le","rhs":1.0,"terms":[[1,1.0],[4,1.0]]},{"cmp":"Le","rhs":1.0,"terms":[[2,1.0],[3,1.0]]}],"num_vars":6,"objective":[[0,1.0],[1,1.0],[2,1.0],[3,1.0],[4,1.0],[5,1.0]],"sense":"Maximize"}},"solutions":[{"source_config":[1,1,1,0,0,0],"target_config":[1,1,1,0,0,0]}]},
Comment on lines 245 to 250
("Factoring", "ILP {variable: \"i32\"}"),
// K3-SAT → QUBO via SAT → CircuitSAT → SpinGlass chain
("KSatisfiability {k: \"K3\"}", "QUBO {weight: \"f64\"}"),
// Knapsack -> ILP -> QUBO is better than the direct penalty reduction
("Knapsack", "QUBO {weight: \"f64\"}"),
// MaxMatching → MaxSetPacking → ILP is better than direct MaxMatching → ILP
Comment on lines +1 to +7
//! Reduction from Knapsack to ILP (Integer Linear Programming).
//!
//! The standard 0-1 knapsack formulation is already a binary ILP:
//! - Variables: one binary variable per item
//! - Constraint: the total selected weight must not exceed capacity
//! - Objective: maximize the total selected value

Comment on lines +54 to +75
#[test]
fn test_knapsack_to_ilp_path_exists() {
let graph = ReductionGraph::new();
let src = ReductionGraph::variant_to_map(&Knapsack::variant());
let dst = ReductionGraph::variant_to_map(&ILP::<bool>::variant());
let path = graph.find_cheapest_path(
"Knapsack",
&src,
"ILP",
&dst,
&ProblemSize::new(vec![]),
&MinimizeSteps,
);

let path = path.expect("Knapsack should reduce to ILP");
assert_eq!(
path.type_names(),
vec!["Knapsack", "ILP"],
"Knapsack should have a direct ILP reduction"
);
assert_eq!(path.len(), 1, "Knapsack -> ILP should be one direct step");
}
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 16, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.48%. Comparing base (7906394) to head (91345cf).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff            @@
##             main     #688    +/-   ##
========================================
  Coverage   97.47%   97.48%            
========================================
  Files         355      357     +2     
  Lines       45294    45418   +124     
========================================
+ Hits        44151    44274   +123     
- Misses       1143     1144     +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@GiggleLiu
Copy link
Copy Markdown
Contributor Author

Agentic Review Report

Structural Check

# Check Status
1 Rule file exists (src/rules/knapsack_ilp.rs) PASS
2 #[reduction(...)] macro present PASS
3 ReductionResult impl present PASS
4 ReduceTo impl present PASS
5 #[cfg(test)] + #[path = "..."] test link PASS
6 Test file exists (src/unit_tests/rules/knapsack_ilp.rs) PASS
7 Closed-loop test present (test_knapsack_to_ilp_closed_loop) PASS
8 Registered in rules/mod.rs PASS
9 Canonical rule example registered PASS
10 Example-db lookup tests exist PASS
11 Paper reduction-rule entry PASS

Build: make test PASS, make clippy PASS

Semantic Review:

  • reduce_to() correctness: OK — Standard textbook mapping: n binary ILP variables, single capacity constraint, value-maximization objective. Traced with canonical example (4 items, weights=[1,3,4,5], values=[1,4,5,7], capacity=7 → optimal [0,1,1,0], value=9). Correct.
  • extract_solution correctness: OK — Identity mapping (1-to-1 Knapsack↔ILP variables).
  • Overhead accuracy: OK — num_vars = "num_items" (verified: ILP constructed with num_vars = self.num_items()), num_constraints = "1" (verified: exactly one capacity inequality).
  • Paper proof sketch: OK — Forward and backward correctness arguments present. Sound and valid.
  • No duplicate registrations: OK — Single ReduceTo<ILP<bool>> for Knapsack impl.
  • Dominated rule update: OK — analysis.rs correctly adds ("Knapsack", "QUBO {weight: \"f64\"}") to known dominated set (Knapsack→ILP→QUBO now dominates direct Knapsack→QUBO).

Issue Compliance: 6/6 passed (source/target, algorithm, solution extraction, correctness, overhead, example all match issue #639).


Quality Check

Design Principles:

  • DRY: OK — Follows same structural pattern as other ILP reductions without copy-pasting. Identity extract_solution is a one-liner shared pattern.
  • KISS: OK — reduce_to() is 18 lines doing exactly one thing. No over-engineering.
  • HC/LC: OK — Single responsibility maintained throughout.

HCI: Not applicable (no CLI/MCP code changed).

Test Quality:

  • 5 dedicated tests: closed-loop round-trip, ILP structure validation (7 assertions), zero-capacity edge case, empty-instance edge case, canonical example spec validation.
  • Plus: graph path test in graph.rs, dominated-rules analysis update in analysis.rs.
  • All tests use meaningful assertions, not shape-only checks.

Issues:

# Severity Finding Location
Q1 Minor i64 as f64 cast can lose precision for values >2^53. Consistent with codebase conventions but Knapsack uses i64 (wider range than i32 in other ILP reductions). src/rules/knapsack_ilp.rs:47,49,55
Q2 Minor Only one non-trivial closed-loop test instance. Boundary tests add coverage, but a second 5+ item instance would strengthen confidence. src/unit_tests/rules/knapsack_ilp.rs:8

Agentic Feature Tests

Test Environment: PR #688 branch, debug + release builds tested.

Test Status Severity
Catalog listing (pred list) Knapsack→ILP missing from catalog critical
Problem details (pred show Knapsack) Only shows QUBO reduction, ILP absent critical
ILP incoming (pred show ILP) 11 incoming reductions listed, Knapsack absent critical
Example creation (pred create --example Knapsack) No canonical model example (pre-existing) info
Solving via brute-force PASS — correct solutions
Reduction (pred reduce ... --to ILP) Works but takes indirect 2-step path (Knapsack→QUBO→ILP) producing 28 vars instead of expected 4 major
Round-trip correctness PASS — solutions map back correctly
Edge cases (empty, single, zero-capacity) PASS
Unit tests (make test) All 2254 pass

Critical Finding: Knapsack→ILP reduction not registered in CLI binary

The inventory::submit! registration generated by #[reduction] is collected in the test binary but not in the CLI binary. Evidence:

  • pred list --rules shows 0 Knapsack→ILP entries
  • pred path Knapsack ILP returns 2-step path through QUBO instead of direct 1-step
  • Unit test test_knapsack_to_ilp_path_exists passes (asserts direct path exists) — test binary and CLI binary disagree
  • The module is properly declared in rules/mod.rs under #[cfg(feature = "ilp-solver")], and the feature IS enabled
  • Other structurally identical ILP reductions (e.g., binpacking_ilp) register correctly

Root cause hypothesis: inventory crate linker-section registration intermittently fails for certain object files when compiled as a library dependency (vs test binary with direct linking). May be exacerbated by LTO on macOS. This appears to be a systemic issue also affecting 2 other reductions in release builds (MinimumFeedbackVertexSet→ILP, MinimumMultiwayCut→ILP).

Pre-existing findings (not from this PR):

  • pred create Knapsack with data flags fails (no match arm in create.rs) — minor
  • Unreachable pattern warning in create.rs:236 — info

Generated by review-pipeline

@isPANN isPANN self-assigned this Mar 20, 2026
isPANN added 2 commits March 20, 2026 14:05
Resolve conflicts:
- references.bib: keep both sides' new entries
- examples.json: remove legacy file (deleted on main)
The direct_ilp_example helper was removed on main. Update to use the
current rule_example_with_witness API with an explicit SolutionPair.
@isPANN isPANN mentioned this pull request Mar 20, 2026
@isPANN isPANN merged commit 418222f into main Mar 20, 2026
5 checks passed
@GiggleLiu GiggleLiu deleted the issue-639-knapsack-to-ilp branch April 12, 2026 00:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Rule] Knapsack to ILP

3 participants