From 388d761dd7082384f2e1388516f906e2348451b2 Mon Sep 17 00:00:00 2001
From: GiggleLiu <cacate0129@gmail.com>
Date: Sat, 31 Jan 2026 10:10:30 +0800
Subject: [PATCH 1/9] docs: Add design document for Factoring to ILP reduction
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Design for issue #21 - integer programming based Factoring solver.
Uses McCormick linearization for binary products with carry propagation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 docs/plans/2026-01-31-factoring-ilp-design.md | 169 ++++++++++++++++++
 1 file changed, 169 insertions(+)
 create mode 100644 docs/plans/2026-01-31-factoring-ilp-design.md

diff --git a/docs/plans/2026-01-31-factoring-ilp-design.md b/docs/plans/2026-01-31-factoring-ilp-design.md
new file mode 100644
index 000000000..3e4bf013a
--- /dev/null
+++ b/docs/plans/2026-01-31-factoring-ilp-design.md
@@ -0,0 +1,169 @@
+# Factoring to ILP Reduction Design
+
+**Issue:** #21 - Implement integer programming based Factoring problem solver
+**Date:** 2026-01-31
+**Status:** Approved
+
+## Overview
+
+Implement a reduction from the Factoring problem to Integer Linear Programming (ILP). Given a target number N and bit widths m and n, find binary factors p and q such that p × q = N.
+
+The key challenge is linearizing the multiplication constraint, which we solve using McCormick relaxation for binary products combined with carry-propagation to handle bit-position sums.
+
+## ILP Formulation
+
+### Variables
+
+| Variable | Range | Count | Description |
+|----------|-------|-------|-------------|
+| `p_i` | {0,1} | m | First factor bits (i = 0..m-1) |
+| `q_j` | {0,1} | n | Second factor bits (j = 0..n-1) |
+| `z_ij` | {0,1} | m×n | Product terms `p_i × q_j` |
+| `c_k` | [0, min(m,n)] | m+n | Carry at bit position k |
+
+**Total variables:** m + n + m×n + (m+n) = O(m×n)
+
+### Constraints
+
+**1. Product linearization** (for each i ∈ [0,m), j ∈ [0,n)):
+```
+z_ij ≤ p_i
+z_ij ≤ q_j
+z_ij ≥ p_i + q_j - 1
+```
+These McCormick constraints ensure z_ij = p_i × q_j for binary variables.
+
+**2. Bit-position equations** (for each k ∈ [0, m+n)):
+```
+Σ_{i+j=k} z_ij + c_{k-1} = N_k + 2·c_k
+```
+Where:
+- N_k is the k-th bit of target N
+- c_{-1} = 0 (no incoming carry at position 0)
+
+**3. No overflow:**
+```
+c_{m+n-1} = 0
+```
+
+**Total constraints:** 3×m×n + (m+n) + 1 = O(m×n)
+
+### Objective
+
+Feasibility problem: minimize 0.
+
+## Implementation
+
+### File Structure
+
+```
+src/rules/factoring_ilp.rs
+```
+
+### Data Structures
+
+```rust
+pub struct ReductionFactoringToILP {
+    target: ILP,
+    source_size: ProblemSize,
+    m: usize,  // bits for first factor
+    n: usize,  // bits for second factor
+}
+```
+
+### Variable Layout
+
+Contiguous indices in ILP:
+```
+[0, m)                    → p_i (first factor bits)
+[m, m+n)                  → q_j (second factor bits)
+[m+n, m+n+m*n)            → z_ij (products, row-major order)
+[m+n+m*n, 2(m+n)+m*n)     → c_k (carries)
+```
+
+### Helper Methods
+
+```rust
+impl ReductionFactoringToILP {
+    fn p_var(&self, i: usize) -> usize { i }
+    fn q_var(&self, j: usize) -> usize { self.m + j }
+    fn z_var(&self, i: usize, j: usize) -> usize {
+        self.m + self.n + i * self.n + j
+    }
+    fn carry_var(&self, k: usize) -> usize {
+        self.m + self.n + self.m * self.n + k
+    }
+}
+```
+
+### Solution Extraction
+
+1. Read p_i values from indices [0, m) → first factor bits
+2. Read q_j values from indices [m, m+n) → second factor bits
+3. Return concatenated bit vector matching Factoring problem format
+
+## Testing Strategy
+
+### Correctness Tests
+
+| Test | Target | Bits | Expected Factors |
+|------|--------|------|------------------|
+| factor_6 | 6 | 2,2 | 2×3 or 3×2 |
+| factor_15 | 15 | 3,3 | 3×5, 5×3, 1×15, 15×1 |
+| factor_35 | 35 | 3,3 | 5×7 or 7×5 |
+
+### Edge Cases
+
+| Test | Description |
+|------|-------------|
+| factor_one | 1 = 1×1 |
+| factor_prime | 7 = 1×7 or 7×1 |
+| factor_square | 9 = 3×3 |
+| infeasible | Target 100 with 2-bit factors (max 9) |
+
+### Validation Tests
+
+- Compare ILP solutions with brute force solver
+- Verify solution extraction produces valid factorization
+- Check variable and constraint counts match formulas
+
+### Closed-Loop Pattern (Issue #3)
+
+```rust
+let problem = Factoring::new(m, n, target);
+let reduction = ReduceTo::<ILP>::reduce_to(&problem);
+let ilp = reduction.target_problem();
+let ilp_solution = ILPSolver::new().solve(ilp)?;
+let extracted = reduction.extract_solution(&ilp_solution);
+assert!(problem.is_valid_factorization(&extracted));
+```
+
+## Documentation Updates
+
+Per issue #3 requirements:
+
+1. Add Factoring → ILP to `docs/paper/reductions.typ`:
+   - Theorem with proof sketch
+   - Add to summary table with overhead O(m×n)
+
+2. Update reduction graph:
+   - Register in inventory
+   - Add ILP node (already exists from Coloring PR)
+   - Add edge Factoring → ILP
+
+3. Regenerate `docs/paper/reduction_graph.json`
+
+## Complexity Analysis
+
+| Metric | Value |
+|--------|-------|
+| Variables | m + n + m×n + (m+n) |
+| Constraints | 3×m×n + (m+n) + 1 |
+| Time complexity | O(m×n) for reduction |
+| Space complexity | O(m×n) |
+
+## References
+
+- McCormick, G. P. (1976). Computability of global solutions to factorable nonconvex programs. Mathematical Programming.
+- Issue #3: Coding rules for AI agents
+- Existing implementation: `src/rules/coloring_ilp.rs` (pattern reference)

From fce3612a56031ee0e5276d1bb485b794ad400b8a Mon Sep 17 00:00:00 2001
From: GiggleLiu <cacate0129@gmail.com>
Date: Sat, 31 Jan 2026 11:13:06 +0800
Subject: [PATCH 2/9] =?UTF-8?q?docs:=20Add=20code=20example=20for=20Factor?=
 =?UTF-8?q?ing=20=E2=86=92=20ILP=20reduction?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Shows closed-loop test pattern: create problem, reduce to ILP,
solve, extract solution, verify correctness.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 docs/paper/reductions.typ | 49 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 49 insertions(+)

diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ
index ccd6cdb53..53dfe46de 100644
--- a/docs/paper/reductions.typ
+++ b/docs/paper/reductions.typ
@@ -64,6 +64,7 @@
   "VertexCovering": (-0.5, 2),
   "Matching": (-2, 2),
   "SpinGlass": (2.5, 2),
+  "ILP": (3.5, 1),
   // Row 3: Leaf nodes
   "SetPacking": (-1.5, 3),
   "SetCovering": (0.5, 3),
@@ -330,6 +331,53 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V|
   _Solution extraction._ Without ancilla: identity. With ancilla: if $sigma_a = 1$, flip all spins before removing ancilla.
 ]
 
+#theorem[
+  *(Factoring $arrow.r$ ILP)* Integer factorization reduces to binary ILP using McCormick linearization with $O(m n)$ variables and constraints.
+]
+
+#proof[
+  _Construction._ For target $N$ with $m$-bit factor $p$ and $n$-bit factor $q$:
+
+  _Variables:_ Binary $p_i, q_j in {0,1}$ for factor bits; binary $z_(i j) in {0,1}$ for products $p_i dot q_j$; integer $c_k >= 0$ for carries at each bit position.
+
+  _Product linearization (McCormick):_ For each $z_(i j) = p_i dot q_j$:
+  $ z_(i j) <= p_i, quad z_(i j) <= q_j, quad z_(i j) >= p_i + q_j - 1 $
+
+  _Bit-position equations:_ For each bit position $k$:
+  $ sum_(i+j=k) z_(i j) + c_(k-1) = N_k + 2 c_k $
+  where $N_k$ is the $k$-th bit of $N$ and $c_(-1) = 0$.
+
+  _No overflow:_ $c_(m+n-1) = 0$.
+
+  _Correctness._ The McCormick constraints enforce $z_(i j) = p_i dot q_j$ for binary variables. The bit equations encode $p times q = N$ via carry propagation, matching array multiplier semantics.
+
+  _Solution extraction._ Read $p = sum_i p_i 2^i$ and $q = sum_j q_j 2^j$ from the binary variables.
+]
+
+_Example: Factoring 15._ The following Rust code demonstrates the closed-loop reduction:
+
+```rust
+use problemreductions::prelude::*;
+
+// 1. Create factoring instance: find p (4-bit) × q (4-bit) = 15
+let problem = Factoring::new(4, 4, 15);
+
+// 2. Reduce to ILP
+let reduction = ReduceTo::<ILP>::reduce_to(&problem);
+let ilp = reduction.target_problem();
+
+// 3. Solve ILP
+let solver = ILPSolver::new();
+let ilp_solution = solver.solve(ilp).unwrap();
+
+// 4. Extract factoring solution
+let extracted = reduction.extract_solution(&ilp_solution);
+
+// 5. Verify: reads factors and confirms p × q = 15
+let (p, q) = problem.read_factors(&extracted);
+assert_eq!(p * q, 15); // e.g., (3, 5) or (5, 3)
+```
+
 = Summary <sec:summary>
 
 #let gray = rgb("#e8e8e8")
@@ -352,6 +400,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V|
     [CircuitSAT $arrow.r$ SpinGlass], [$O(|"gates"|)$], [@whitfield2012 @lucas2014],
     [Factoring $arrow.r$ CircuitSAT], [$O(m n)$], [Folklore],
     [SpinGlass $arrow.l.r$ MaxCut], [$O(n + |J|)$], [@barahona1982 @lucas2014],
+    table.cell(fill: gray)[Factoring $arrow.r$ ILP], table.cell(fill: gray)[$O(m n)$], table.cell(fill: gray)[—],
   ),
   caption: [Summary of reductions. Gray rows indicate trivial reductions.]
 ) <tab:summary>

From a8123a6a47c0e8a3ad34b0b6ce79109e1d7f62d4 Mon Sep 17 00:00:00 2001
From: GiggleLiu <cacate0129@gmail.com>
Date: Sat, 31 Jan 2026 11:13:52 +0800
Subject: [PATCH 3/9] test: Add step-by-step comments to factor_15 test
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Documents the closed-loop test pattern matching the typst example.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 src/rules/factoring_ilp.rs | 579 +++++++++++++++++++++++++++++++++++++
 1 file changed, 579 insertions(+)
 create mode 100644 src/rules/factoring_ilp.rs

diff --git a/src/rules/factoring_ilp.rs b/src/rules/factoring_ilp.rs
new file mode 100644
index 000000000..77b05b8b7
--- /dev/null
+++ b/src/rules/factoring_ilp.rs
@@ -0,0 +1,579 @@
+//! Reduction from Factoring to ILP (Integer Linear Programming).
+//!
+//! The Integer Factoring problem can be formulated as a binary ILP using
+//! McCormick linearization for binary products combined with carry propagation.
+//!
+//! Given target N and bit widths m, n, find factors p (m bits) and q (n bits)
+//! such that p × q = N.
+//!
+//! ## Variables
+//! - `p_i ∈ {0,1}` for i = 0..m-1 (first factor bits)
+//! - `q_j ∈ {0,1}` for j = 0..n-1 (second factor bits)
+//! - `z_ij ∈ {0,1}` for each (i,j) pair (product p_i × q_j)
+//! - `c_k ∈ ℤ≥0` for k = 0..m+n-1 (carry at each bit position)
+//!
+//! ## Constraints
+//! 1. Product linearization (McCormick): z_ij ≤ p_i, z_ij ≤ q_j, z_ij ≥ p_i + q_j - 1
+//! 2. Bit-position sums: Σ_{i+j=k} z_ij + c_{k-1} = N_k + 2·c_k
+//! 3. No overflow: c_{m+n-1} = 0
+
+use crate::models::optimization::{ILP, LinearConstraint, ObjectiveSense, VarBounds};
+use crate::models::specialized::Factoring;
+use crate::polynomial::{Monomial, Polynomial};
+use crate::rules::registry::{ReductionEntry, ReductionOverhead};
+use crate::rules::traits::{ReduceTo, ReductionResult};
+use crate::traits::Problem;
+use crate::types::ProblemSize;
+use std::cmp::min;
+
+// Register reduction in the inventory for automatic discovery
+inventory::submit! {
+    ReductionEntry {
+        source_name: "Factoring",
+        target_name: "ILP",
+        source_graph: "Factoring",
+        target_graph: "ILPMatrix",
+        overhead_fn: || ReductionOverhead::new(vec![
+            // num_vars = m + n + m*n + (m+n) = 2(m+n) + m*n
+            ("num_vars", Polynomial {
+                terms: vec![
+                    Monomial::var("num_bits_first").scale(2.0),
+                    Monomial::var("num_bits_second").scale(2.0),
+                    Monomial {
+                        coefficient: 1.0,
+                        variables: vec![("num_bits_first", 1), ("num_bits_second", 1)],
+                    },
+                ]
+            }),
+            // num_constraints = 3*m*n + (m+n) + 1
+            ("num_constraints", Polynomial {
+                terms: vec![
+                    Monomial {
+                        coefficient: 3.0,
+                        variables: vec![("num_bits_first", 1), ("num_bits_second", 1)],
+                    },
+                    Monomial::var("num_bits_first"),
+                    Monomial::var("num_bits_second"),
+                    Monomial::constant(1.0),
+                ]
+            }),
+        ]),
+    }
+}
+
+/// Result of reducing Factoring to ILP.
+///
+/// This reduction creates an ILP where:
+/// - Binary variables represent factor bits and their products
+/// - Integer variables represent carries at each bit position
+/// - Constraints enforce the multiplication equals the target
+#[derive(Debug, Clone)]
+pub struct ReductionFactoringToILP {
+    target: ILP,
+    source_size: ProblemSize,
+    m: usize, // bits for first factor
+    n: usize, // bits for second factor
+}
+
+impl ReductionFactoringToILP {
+    /// Get the variable index for p_i (first factor bit i).
+    fn p_var(&self, i: usize) -> usize {
+        i
+    }
+
+    /// Get the variable index for q_j (second factor bit j).
+    fn q_var(&self, j: usize) -> usize {
+        self.m + j
+    }
+
+    /// Get the variable index for z_ij (product p_i × q_j).
+    #[cfg(test)]
+    fn z_var(&self, i: usize, j: usize) -> usize {
+        self.m + self.n + i * self.n + j
+    }
+
+    /// Get the variable index for carry at position k.
+    #[cfg(test)]
+    fn carry_var(&self, k: usize) -> usize {
+        self.m + self.n + self.m * self.n + k
+    }
+}
+
+impl ReductionResult for ReductionFactoringToILP {
+    type Source = Factoring;
+    type Target = ILP;
+
+    fn target_problem(&self) -> &ILP {
+        &self.target
+    }
+
+    /// Extract solution from ILP back to Factoring.
+    ///
+    /// The first m variables are p_i (first factor bits).
+    /// The next n variables are q_j (second factor bits).
+    /// Returns concatenated bit vector [p_0, ..., p_{m-1}, q_0, ..., q_{n-1}].
+    fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
+        // Extract p bits (first factor)
+        let p_bits: Vec<usize> = (0..self.m)
+            .map(|i| target_solution.get(self.p_var(i)).copied().unwrap_or(0))
+            .collect();
+
+        // Extract q bits (second factor)
+        let q_bits: Vec<usize> = (0..self.n)
+            .map(|j| target_solution.get(self.q_var(j)).copied().unwrap_or(0))
+            .collect();
+
+        // Concatenate p and q bits
+        let mut result = p_bits;
+        result.extend(q_bits);
+        result
+    }
+
+    fn source_size(&self) -> ProblemSize {
+        self.source_size.clone()
+    }
+
+    fn target_size(&self) -> ProblemSize {
+        self.target.problem_size()
+    }
+}
+
+impl ReduceTo<ILP> for Factoring {
+    type Result = ReductionFactoringToILP;
+
+    fn reduce_to(&self) -> Self::Result {
+        let m = self.m();
+        let n = self.n();
+        let target = self.target();
+
+        // Calculate the number of bits needed for the target
+        let target_bits = if target == 0 {
+            1
+        } else {
+            (64 - target.leading_zeros()) as usize
+        };
+
+        // Number of bit positions to check: max(m+n, target_bits)
+        // This ensures we catch infeasible cases where target is too large
+        let num_bit_positions = std::cmp::max(m + n, target_bits);
+
+        // Total variables: m + n + m*n + num_bit_positions
+        let num_p = m;
+        let num_q = n;
+        let num_z = m * n;
+        let num_carries = num_bit_positions;
+        let num_vars = num_p + num_q + num_z + num_carries;
+
+        // Helper functions for variable indices
+        let p_var = |i: usize| -> usize { i };
+        let q_var = |j: usize| -> usize { m + j };
+        let z_var = |i: usize, j: usize| -> usize { m + n + i * n + j };
+        let carry_var = |k: usize| -> usize { m + n + m * n + k };
+
+        // Variable bounds
+        let mut bounds = Vec::with_capacity(num_vars);
+
+        // p_i, q_j, z_ij are binary
+        for _ in 0..(num_p + num_q + num_z) {
+            bounds.push(VarBounds::binary());
+        }
+
+        // c_k are non-negative integers with upper bound min(m, n)
+        // (at most min(m, n) products can contribute to any position)
+        let carry_upper = min(m, n) as i64;
+        for _ in 0..num_carries {
+            bounds.push(VarBounds::bounded(0, carry_upper));
+        }
+
+        let mut constraints = Vec::new();
+
+        // Constraint 1: Product linearization (McCormick constraints)
+        // For each z_ij = p_i × q_j:
+        //   z_ij ≤ p_i
+        //   z_ij ≤ q_j
+        //   z_ij ≥ p_i + q_j - 1
+        for i in 0..m {
+            for j in 0..n {
+                let z = z_var(i, j);
+                let p = p_var(i);
+                let q = q_var(j);
+
+                // z_ij - p_i ≤ 0
+                constraints.push(LinearConstraint::le(
+                    vec![(z, 1.0), (p, -1.0)],
+                    0.0,
+                ));
+
+                // z_ij - q_j ≤ 0
+                constraints.push(LinearConstraint::le(
+                    vec![(z, 1.0), (q, -1.0)],
+                    0.0,
+                ));
+
+                // z_ij - p_i - q_j ≥ -1
+                constraints.push(LinearConstraint::ge(
+                    vec![(z, 1.0), (p, -1.0), (q, -1.0)],
+                    -1.0,
+                ));
+            }
+        }
+
+        // Constraint 2: Bit-position equations
+        // For each bit position k = 0..num_bit_positions-1:
+        //   Σ_{i+j=k} z_ij + c_{k-1} = N_k + 2·c_k
+        // Rearranged: Σ_{i+j=k} z_ij + c_{k-1} - 2·c_k = N_k
+        for k in 0..num_bit_positions {
+            let mut terms: Vec<(usize, f64)> = Vec::new();
+
+            // Collect all z_ij where i + j = k
+            for i in 0..m {
+                if k >= i && k - i < n {
+                    let j = k - i;
+                    terms.push((z_var(i, j), 1.0));
+                }
+            }
+
+            // Add carry_in (from position k-1)
+            if k > 0 {
+                terms.push((carry_var(k - 1), 1.0));
+            }
+
+            // Subtract 2 × carry_out
+            terms.push((carry_var(k), -2.0));
+
+            // RHS is N_k (k-th bit of target)
+            let n_k = ((target >> k) & 1) as f64;
+            constraints.push(LinearConstraint::eq(terms, n_k));
+        }
+
+        // Constraint 3: Final carry must be zero (no overflow)
+        constraints.push(LinearConstraint::eq(
+            vec![(carry_var(num_bit_positions - 1), 1.0)],
+            0.0,
+        ));
+
+        // Objective: feasibility problem (minimize 0)
+        let objective: Vec<(usize, f64)> = vec![];
+
+        let ilp = ILP::new(
+            num_vars,
+            bounds,
+            constraints,
+            objective,
+            ObjectiveSense::Minimize,
+        );
+
+        ReductionFactoringToILP {
+            target: ilp,
+            source_size: self.problem_size(),
+            m,
+            n,
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::solvers::{BruteForce, ILPSolver, Solver};
+
+    #[test]
+    fn test_reduction_creates_valid_ilp() {
+        // Factor 6 with 2-bit factors
+        let problem = Factoring::new(2, 2, 6);
+        let reduction: ReductionFactoringToILP = ReduceTo::<ILP>::reduce_to(&problem);
+        let ilp = reduction.target_problem();
+
+        // Check variable count: m + n + m*n + (m+n) = 2 + 2 + 4 + 4 = 12
+        assert_eq!(ilp.num_vars, 12);
+
+        // Check constraint count: 3*m*n + (m+n) + 1 = 12 + 4 + 1 = 17
+        assert_eq!(ilp.constraints.len(), 17);
+
+        assert_eq!(ilp.sense, ObjectiveSense::Minimize);
+    }
+
+    #[test]
+    fn test_variable_layout() {
+        let problem = Factoring::new(3, 2, 6);
+        let reduction: ReductionFactoringToILP = ReduceTo::<ILP>::reduce_to(&problem);
+
+        // p variables: [0, 1, 2]
+        assert_eq!(reduction.p_var(0), 0);
+        assert_eq!(reduction.p_var(2), 2);
+
+        // q variables: [3, 4]
+        assert_eq!(reduction.q_var(0), 3);
+        assert_eq!(reduction.q_var(1), 4);
+
+        // z variables: [5, 6, 7, 8, 9, 10] (3x2 = 6)
+        assert_eq!(reduction.z_var(0, 0), 5);
+        assert_eq!(reduction.z_var(0, 1), 6);
+        assert_eq!(reduction.z_var(1, 0), 7);
+        assert_eq!(reduction.z_var(2, 1), 10);
+
+        // carry variables: [11, 12, 13, 14, 15] (m+n = 5)
+        assert_eq!(reduction.carry_var(0), 11);
+        assert_eq!(reduction.carry_var(4), 15);
+    }
+
+    #[test]
+    fn test_factor_6() {
+        // 6 = 2 × 3 or 3 × 2
+        let problem = Factoring::new(2, 2, 6);
+        let reduction: ReductionFactoringToILP = ReduceTo::<ILP>::reduce_to(&problem);
+        let ilp = reduction.target_problem();
+
+        let ilp_solver = ILPSolver::new();
+        let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable");
+        let extracted = reduction.extract_solution(&ilp_solution);
+
+        // Verify it's a valid factorization
+        assert!(problem.is_valid_factorization(&extracted));
+
+        let (a, b) = problem.read_factors(&extracted);
+        assert_eq!(a * b, 6);
+    }
+
+    #[test]
+    fn test_factor_15() {
+        // Closed-loop test for factoring 15 = 3 × 5 (or 5 × 3, 1 × 15, 15 × 1)
+
+        // 1. Create factoring instance: find p (4-bit) × q (4-bit) = 15
+        let problem = Factoring::new(4, 4, 15);
+
+        // 2. Reduce to ILP
+        let reduction: ReductionFactoringToILP = ReduceTo::<ILP>::reduce_to(&problem);
+        let ilp = reduction.target_problem();
+
+        // 3. Solve ILP
+        let ilp_solver = ILPSolver::new();
+        let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable");
+
+        // 4. Extract factoring solution
+        let extracted = reduction.extract_solution(&ilp_solution);
+
+        // 5. Verify: solution is valid and p × q = 15
+        assert!(problem.is_valid_factorization(&extracted));
+        let (p, q) = problem.read_factors(&extracted);
+        assert_eq!(p * q, 15); // e.g., (3, 5) or (5, 3)
+    }
+
+    #[test]
+    fn test_factor_35() {
+        // 35 = 5 × 7 or 7 × 5
+        let problem = Factoring::new(3, 3, 35);
+        let reduction: ReductionFactoringToILP = ReduceTo::<ILP>::reduce_to(&problem);
+        let ilp = reduction.target_problem();
+
+        let ilp_solver = ILPSolver::new();
+        let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable");
+        let extracted = reduction.extract_solution(&ilp_solution);
+
+        assert!(problem.is_valid_factorization(&extracted));
+
+        let (a, b) = problem.read_factors(&extracted);
+        assert_eq!(a * b, 35);
+    }
+
+    #[test]
+    fn test_factor_one() {
+        // 1 = 1 × 1
+        let problem = Factoring::new(2, 2, 1);
+        let reduction: ReductionFactoringToILP = ReduceTo::<ILP>::reduce_to(&problem);
+        let ilp = reduction.target_problem();
+
+        let ilp_solver = ILPSolver::new();
+        let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable");
+        let extracted = reduction.extract_solution(&ilp_solution);
+
+        assert!(problem.is_valid_factorization(&extracted));
+
+        let (a, b) = problem.read_factors(&extracted);
+        assert_eq!(a * b, 1);
+    }
+
+    #[test]
+    fn test_factor_prime() {
+        // 7 is prime: 7 = 1 × 7 or 7 × 1
+        let problem = Factoring::new(3, 3, 7);
+        let reduction: ReductionFactoringToILP = ReduceTo::<ILP>::reduce_to(&problem);
+        let ilp = reduction.target_problem();
+
+        let ilp_solver = ILPSolver::new();
+        let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable");
+        let extracted = reduction.extract_solution(&ilp_solution);
+
+        assert!(problem.is_valid_factorization(&extracted));
+
+        let (a, b) = problem.read_factors(&extracted);
+        assert_eq!(a * b, 7);
+    }
+
+    #[test]
+    fn test_factor_square() {
+        // 9 = 3 × 3
+        let problem = Factoring::new(3, 3, 9);
+        let reduction: ReductionFactoringToILP = ReduceTo::<ILP>::reduce_to(&problem);
+        let ilp = reduction.target_problem();
+
+        let ilp_solver = ILPSolver::new();
+        let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable");
+        let extracted = reduction.extract_solution(&ilp_solution);
+
+        assert!(problem.is_valid_factorization(&extracted));
+
+        let (a, b) = problem.read_factors(&extracted);
+        assert_eq!(a * b, 9);
+    }
+
+    #[test]
+    fn test_infeasible_target_too_large() {
+        // Target 100 with 2-bit factors (max product is 3 × 3 = 9)
+        let problem = Factoring::new(2, 2, 100);
+        let reduction: ReductionFactoringToILP = ReduceTo::<ILP>::reduce_to(&problem);
+        let ilp = reduction.target_problem();
+
+        let ilp_solver = ILPSolver::new();
+        let result = ilp_solver.solve(ilp);
+
+        assert!(result.is_none(), "Should be infeasible");
+    }
+
+    #[test]
+    fn test_ilp_matches_brute_force() {
+        let problem = Factoring::new(2, 2, 6);
+        let reduction: ReductionFactoringToILP = ReduceTo::<ILP>::reduce_to(&problem);
+        let ilp = reduction.target_problem();
+
+        // Get ILP solution
+        let ilp_solver = ILPSolver::new();
+        let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable");
+        let ilp_factors = reduction.extract_solution(&ilp_solution);
+
+        // Get brute force solutions
+        let bf = BruteForce::new();
+        let bf_solutions = bf.find_best(&problem);
+
+        // ILP solution should be among brute force solutions
+        let (a, b) = problem.read_factors(&ilp_factors);
+        let bf_pairs: Vec<(u64, u64)> = bf_solutions
+            .iter()
+            .map(|s| problem.read_factors(s))
+            .collect();
+
+        assert!(
+            bf_pairs.contains(&(a, b)),
+            "ILP solution ({}, {}) should be in brute force solutions {:?}",
+            a,
+            b,
+            bf_pairs
+        );
+    }
+
+    #[test]
+    fn test_solution_extraction() {
+        let problem = Factoring::new(2, 2, 6);
+        let reduction: ReductionFactoringToILP = ReduceTo::<ILP>::reduce_to(&problem);
+
+        // Manually construct ILP solution for 2 × 3 = 6
+        // p = 2 = binary 10 -> p_0=0, p_1=1
+        // q = 3 = binary 11 -> q_0=1, q_1=1
+        // z_00 = p_0 * q_0 = 0, z_01 = p_0 * q_1 = 0
+        // z_10 = p_1 * q_0 = 1, z_11 = p_1 * q_1 = 1
+        // Variables: [p0, p1, q0, q1, z00, z01, z10, z11, c0, c1, c2, c3]
+        let ilp_solution = vec![0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0];
+        let extracted = reduction.extract_solution(&ilp_solution);
+
+        // Should extract [p0, p1, q0, q1] = [0, 1, 1, 1]
+        assert_eq!(extracted, vec![0, 1, 1, 1]);
+
+        let (a, b) = problem.read_factors(&extracted);
+        assert_eq!(a, 2);
+        assert_eq!(b, 3);
+        assert_eq!(a * b, 6);
+    }
+
+    #[test]
+    fn test_source_and_target_size() {
+        let problem = Factoring::new(3, 4, 12);
+        let reduction: ReductionFactoringToILP = ReduceTo::<ILP>::reduce_to(&problem);
+
+        let source_size = reduction.source_size();
+        let target_size = reduction.target_size();
+
+        assert_eq!(source_size.get("num_bits_first"), Some(3));
+        assert_eq!(source_size.get("num_bits_second"), Some(4));
+
+        // num_vars = 3 + 4 + 12 + 7 = 26
+        assert_eq!(target_size.get("num_vars"), Some(26));
+
+        // num_constraints = 3*12 + 7 + 1 = 44
+        assert_eq!(target_size.get("num_constraints"), Some(44));
+    }
+
+    #[test]
+    fn test_solve_reduced() {
+        let problem = Factoring::new(2, 2, 6);
+
+        let ilp_solver = ILPSolver::new();
+        let solution = ilp_solver
+            .solve_reduced(&problem)
+            .expect("solve_reduced should work");
+
+        assert!(problem.is_valid_factorization(&solution));
+    }
+
+    #[test]
+    fn test_asymmetric_bit_widths() {
+        // 12 = 3 × 4 or 4 × 3 or 2 × 6 or 6 × 2 or 1 × 12 or 12 × 1
+        let problem = Factoring::new(2, 4, 12);
+        let reduction: ReductionFactoringToILP = ReduceTo::<ILP>::reduce_to(&problem);
+        let ilp = reduction.target_problem();
+
+        let ilp_solver = ILPSolver::new();
+        let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable");
+        let extracted = reduction.extract_solution(&ilp_solution);
+
+        assert!(problem.is_valid_factorization(&extracted));
+
+        let (a, b) = problem.read_factors(&extracted);
+        assert_eq!(a * b, 12);
+    }
+
+    #[test]
+    fn test_constraint_count_formula() {
+        // Verify constraint count matches formula: 3*m*n + (m+n) + 1
+        for (m, n) in [(2, 2), (3, 3), (2, 4), (4, 2)] {
+            let problem = Factoring::new(m, n, 1);
+            let reduction: ReductionFactoringToILP = ReduceTo::<ILP>::reduce_to(&problem);
+            let ilp = reduction.target_problem();
+
+            let expected = 3 * m * n + (m + n) + 1;
+            assert_eq!(
+                ilp.constraints.len(),
+                expected,
+                "Constraint count mismatch for m={}, n={}",
+                m,
+                n
+            );
+        }
+    }
+
+    #[test]
+    fn test_variable_count_formula() {
+        // Verify variable count matches formula: m + n + m*n + (m+n)
+        for (m, n) in [(2, 2), (3, 3), (2, 4), (4, 2)] {
+            let problem = Factoring::new(m, n, 1);
+            let reduction: ReductionFactoringToILP = ReduceTo::<ILP>::reduce_to(&problem);
+            let ilp = reduction.target_problem();
+
+            let expected = m + n + m * n + (m + n);
+            assert_eq!(
+                ilp.num_vars, expected,
+                "Variable count mismatch for m={}, n={}",
+                m, n
+            );
+        }
+    }
+}

From 3a6850a249f645c182254bfeb2cce541da54315f Mon Sep 17 00:00:00 2001
From: GiggleLiu <cacate0129@gmail.com>
Date: Sat, 31 Jan 2026 11:14:02 +0800
Subject: [PATCH 4/9] =?UTF-8?q?feat:=20Integrate=20Factoring=20=E2=86=92?=
 =?UTF-8?q?=20ILP=20into=20reduction=20graph?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- Register reduction in mod.rs
- Add edge in graph.rs type registry
- Update reduction graph documentation
- Regenerate reduction_graph.json

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 docs/paper/reduction_graph.json | 118 +++++++++++++++++---------------
 docs/src/reductions/graph.md    |   4 ++
 src/rules/graph.rs              |   6 +-
 src/rules/mod.rs                |   4 ++
 4 files changed, 77 insertions(+), 55 deletions(-)

diff --git a/docs/paper/reduction_graph.json b/docs/paper/reduction_graph.json
index d9a6c40a6..64c7407e9 100644
--- a/docs/paper/reduction_graph.json
+++ b/docs/paper/reduction_graph.json
@@ -1,8 +1,8 @@
 {
   "nodes": [
     {
-      "id": "MaxCut",
-      "label": "MaxCut",
+      "id": "IndependentSet",
+      "label": "IndependentSet",
       "category": "graph"
     },
     {
@@ -11,29 +11,14 @@
       "category": "graph"
     },
     {
-      "id": "SpinGlass",
-      "label": "SpinGlass",
-      "category": "optimization"
-    },
-    {
-      "id": "QUBO",
-      "label": "QUBO",
-      "category": "optimization"
-    },
-    {
-      "id": "IndependentSet",
-      "label": "IndependentSet",
+      "id": "VertexCovering",
+      "label": "VertexCovering",
       "category": "graph"
     },
     {
-      "id": "SetPacking",
-      "label": "SetPacking",
-      "category": "set"
-    },
-    {
-      "id": "Satisfiability",
-      "label": "Satisfiability",
-      "category": "satisfiability"
+      "id": "Matching",
+      "label": "Matching",
+      "category": "graph"
     },
     {
       "id": "Coloring",
@@ -41,9 +26,9 @@
       "category": "graph"
     },
     {
-      "id": "VertexCovering",
-      "label": "VertexCovering",
-      "category": "graph"
+      "id": "ILP",
+      "label": "ILP",
+      "category": "optimization"
     },
     {
       "id": "KSatisfiability",
@@ -55,26 +40,51 @@
       "label": "CircuitSAT",
       "category": "satisfiability"
     },
+    {
+      "id": "SetCovering",
+      "label": "SetCovering",
+      "category": "set"
+    },
     {
       "id": "Factoring",
       "label": "Factoring",
       "category": "specialized"
     },
     {
-      "id": "Matching",
-      "label": "Matching",
+      "id": "MaxCut",
+      "label": "MaxCut",
       "category": "graph"
     },
     {
-      "id": "SetCovering",
-      "label": "SetCovering",
+      "id": "SpinGlass",
+      "label": "SpinGlass",
+      "category": "optimization"
+    },
+    {
+      "id": "SetPacking",
+      "label": "SetPacking",
       "category": "set"
+    },
+    {
+      "id": "QUBO",
+      "label": "QUBO",
+      "category": "optimization"
+    },
+    {
+      "id": "Satisfiability",
+      "label": "Satisfiability",
+      "category": "satisfiability"
     }
   ],
   "edges": [
     {
-      "source": "CircuitSAT",
-      "target": "SpinGlass",
+      "source": "SetPacking",
+      "target": "IndependentSet",
+      "bidirectional": true
+    },
+    {
+      "source": "Satisfiability",
+      "target": "DominatingSet",
       "bidirectional": false
     },
     {
@@ -83,30 +93,20 @@
       "bidirectional": false
     },
     {
-      "source": "Satisfiability",
-      "target": "KSatisfiability",
-      "bidirectional": true
-    },
-    {
-      "source": "Factoring",
-      "target": "CircuitSAT",
+      "source": "CircuitSAT",
+      "target": "SpinGlass",
       "bidirectional": false
     },
     {
-      "source": "IndependentSet",
-      "target": "SetPacking",
-      "bidirectional": true
+      "source": "Satisfiability",
+      "target": "IndependentSet",
+      "bidirectional": false
     },
     {
       "source": "SpinGlass",
       "target": "QUBO",
       "bidirectional": true
     },
-    {
-      "source": "MaxCut",
-      "target": "SpinGlass",
-      "bidirectional": true
-    },
     {
       "source": "VertexCovering",
       "target": "SetCovering",
@@ -114,23 +114,33 @@
     },
     {
       "source": "Satisfiability",
-      "target": "IndependentSet",
+      "target": "Coloring",
       "bidirectional": false
     },
     {
-      "source": "Satisfiability",
-      "target": "Coloring",
+      "source": "Factoring",
+      "target": "ILP",
+      "bidirectional": false
+    },
+    {
+      "source": "Factoring",
+      "target": "CircuitSAT",
       "bidirectional": false
     },
     {
-      "source": "IndependentSet",
-      "target": "VertexCovering",
+      "source": "SpinGlass",
+      "target": "MaxCut",
       "bidirectional": true
     },
     {
-      "source": "Satisfiability",
-      "target": "DominatingSet",
-      "bidirectional": false
+      "source": "KSatisfiability",
+      "target": "Satisfiability",
+      "bidirectional": true
+    },
+    {
+      "source": "VertexCovering",
+      "target": "IndependentSet",
+      "bidirectional": true
     }
   ]
 }
\ No newline at end of file
diff --git a/docs/src/reductions/graph.md b/docs/src/reductions/graph.md
index a737470eb..00044cefb 100644
--- a/docs/src/reductions/graph.md
+++ b/docs/src/reductions/graph.md
@@ -37,10 +37,12 @@ flowchart TD
         SG_f64["SpinGlass&lt;f64&gt;"]
         QUBO["QUBO&lt;f64&gt;"]
         MaxCut["MaxCut&lt;i32&gt;"]
+        ILP["ILP"]
     end
 
     %% Factoring chain
     Factoring --> CircuitSAT
+    Factoring --> ILP
     CircuitSAT --> SG_i32
 
     %% SAT reductions
@@ -75,6 +77,7 @@ flowchart TD
     style SG_f64 fill:#ff9,stroke:#333
     style QUBO fill:#ff9,stroke:#333
     style MaxCut fill:#ff9,stroke:#333
+    style ILP fill:#ff9,stroke:#333
 ```
 
 ## Legend
@@ -126,6 +129,7 @@ println!("Types: {}, Reductions: {}", graph.num_types(), graph.num_reductions())
 | Satisfiability | DominatingSet | No |
 | CircuitSAT | SpinGlass&lt;i32&gt; | No |
 | Factoring | CircuitSAT | No |
+| Factoring | ILP | No |
 
 ## API
 
diff --git a/src/rules/graph.rs b/src/rules/graph.rs
index d96c53e0b..8649b720b 100644
--- a/src/rules/graph.rs
+++ b/src/rules/graph.rs
@@ -260,6 +260,7 @@ impl ReductionGraph {
             SpinGlass<i32> => "SpinGlass",
             SpinGlass<f64> => "SpinGlass",
             QUBO<f64> => "QUBO",
+            ILP => "ILP",
             // Satisfiability problems
             Satisfiability<i32> => "Satisfiability",
             KSatisfiability<3, i32> => "KSatisfiability",
@@ -318,6 +319,9 @@ impl ReductionGraph {
         // Circuit reductions
         add_edge!("CircuitSAT" => "SpinGlass");
         add_edge!("Factoring" => "CircuitSAT");
+
+        // ILP reductions (for solver targets)
+        add_edge!("Factoring" => "ILP");
     }
 
     /// Check if `sub` is a subtype of `sup` (or equal).
@@ -624,7 +628,7 @@ impl ReductionGraph {
             "graph"
         } else if name.contains("SetPacking") || name.contains("SetCover") {
             "set"
-        } else if name.contains("SpinGlass") || name.contains("QUBO") {
+        } else if name.contains("SpinGlass") || name.contains("QUBO") || name.contains("ILP") {
             "optimization"
         } else if name.contains("Satisfiability") || name.contains("SAT") {
             "satisfiability"
diff --git a/src/rules/mod.rs b/src/rules/mod.rs
index ac502adca..fecbfd010 100644
--- a/src/rules/mod.rs
+++ b/src/rules/mod.rs
@@ -34,6 +34,8 @@ mod setcovering_ilp;
 mod setpacking_ilp;
 #[cfg(feature = "ilp")]
 mod vertexcovering_ilp;
+#[cfg(feature = "ilp")]
+mod factoring_ilp;
 
 pub use graph::{EdgeJson, NodeJson, ReductionEdge, ReductionGraph, ReductionGraphJson, ReductionPath};
 pub use traits::{ReduceTo, ReductionResult};
@@ -67,3 +69,5 @@ pub use setcovering_ilp::ReductionSCToILP;
 pub use setpacking_ilp::ReductionSPToILP;
 #[cfg(feature = "ilp")]
 pub use vertexcovering_ilp::ReductionVCToILP;
+#[cfg(feature = "ilp")]
+pub use factoring_ilp::ReductionFactoringToILP;

From d127c0ebdb1802aacad7e2b85e799b513e61f16b Mon Sep 17 00:00:00 2001
From: GiggleLiu <cacate0129@gmail.com>
Date: Sat, 31 Jan 2026 11:28:02 +0800
Subject: [PATCH 5/9] fix: Address PR review comments
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- Fix design doc reference to use existing vertexcovering_ilp.rs
- Add #[cfg(feature = "ilp")] gate to Factoring→ILP edge in graph.rs
- Add shift overflow guard for k >= 64 in bit extraction
- Add feature requirement note to typst example
- Clarify overhead formula comments for infeasible cases

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 docs/paper/reductions.typ                     |  2 +-
 docs/plans/2026-01-31-factoring-ilp-design.md |  2 +-
 src/rules/factoring_ilp.rs                    | 18 +++++++++++++-----
 src/rules/graph.rs                            |  1 +
 4 files changed, 16 insertions(+), 7 deletions(-)

diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ
index 53dfe46de..601289af6 100644
--- a/docs/paper/reductions.typ
+++ b/docs/paper/reductions.typ
@@ -354,7 +354,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V|
   _Solution extraction._ Read $p = sum_i p_i 2^i$ and $q = sum_j q_j 2^j$ from the binary variables.
 ]
 
-_Example: Factoring 15._ The following Rust code demonstrates the closed-loop reduction:
+_Example: Factoring 15._ The following Rust code demonstrates the closed-loop reduction (requires `ilp` feature: `cargo add problemreductions --features ilp`):
 
 ```rust
 use problemreductions::prelude::*;
diff --git a/docs/plans/2026-01-31-factoring-ilp-design.md b/docs/plans/2026-01-31-factoring-ilp-design.md
index 3e4bf013a..2c0496282 100644
--- a/docs/plans/2026-01-31-factoring-ilp-design.md
+++ b/docs/plans/2026-01-31-factoring-ilp-design.md
@@ -166,4 +166,4 @@ Per issue #3 requirements:
 
 - McCormick, G. P. (1976). Computability of global solutions to factorable nonconvex programs. Mathematical Programming.
 - Issue #3: Coding rules for AI agents
-- Existing implementation: `src/rules/coloring_ilp.rs` (pattern reference)
+- Existing implementation: `src/rules/vertexcovering_ilp.rs` (pattern reference)
diff --git a/src/rules/factoring_ilp.rs b/src/rules/factoring_ilp.rs
index 77b05b8b7..055cb95f9 100644
--- a/src/rules/factoring_ilp.rs
+++ b/src/rules/factoring_ilp.rs
@@ -34,7 +34,8 @@ inventory::submit! {
         source_graph: "Factoring",
         target_graph: "ILPMatrix",
         overhead_fn: || ReductionOverhead::new(vec![
-            // num_vars = m + n + m*n + (m+n) = 2(m+n) + m*n
+            // num_vars = m + n + m*n + num_carries where num_carries = max(m+n, target_bits)
+            // For feasible instances, target_bits <= m+n, so this is 2(m+n) + m*n
             ("num_vars", Polynomial {
                 terms: vec![
                     Monomial::var("num_bits_first").scale(2.0),
@@ -45,7 +46,8 @@ inventory::submit! {
                     },
                 ]
             }),
-            // num_constraints = 3*m*n + (m+n) + 1
+            // num_constraints = 3*m*n + num_bit_positions + 1
+            // For feasible instances (target_bits <= m+n), this is 3*m*n + (m+n) + 1
             ("num_constraints", Polynomial {
                 terms: vec![
                     Monomial {
@@ -154,7 +156,9 @@ impl ReduceTo<ILP> for Factoring {
         };
 
         // Number of bit positions to check: max(m+n, target_bits)
-        // This ensures we catch infeasible cases where target is too large
+        // For feasible instances, target_bits <= m+n (product of m-bit × n-bit has at most m+n bits).
+        // When target_bits > m+n, the ILP will be infeasible (target too large for given bit widths).
+        // Using max() here ensures proper infeasibility detection through the bit equations.
         let num_bit_positions = std::cmp::max(m + n, target_bits);
 
         // Total variables: m + n + m*n + num_bit_positions
@@ -241,8 +245,12 @@ impl ReduceTo<ILP> for Factoring {
             // Subtract 2 × carry_out
             terms.push((carry_var(k), -2.0));
 
-            // RHS is N_k (k-th bit of target)
-            let n_k = ((target >> k) & 1) as f64;
+            // RHS is N_k (k-th bit of target). For k >= 64, the bit is 0 for u64.
+            let n_k = if k < 64 {
+                ((target >> k) & 1) as f64
+            } else {
+                0.0
+            };
             constraints.push(LinearConstraint::eq(terms, n_k));
         }
 
diff --git a/src/rules/graph.rs b/src/rules/graph.rs
index 8649b720b..33a2366c6 100644
--- a/src/rules/graph.rs
+++ b/src/rules/graph.rs
@@ -321,6 +321,7 @@ impl ReductionGraph {
         add_edge!("Factoring" => "CircuitSAT");
 
         // ILP reductions (for solver targets)
+        #[cfg(feature = "ilp")]
         add_edge!("Factoring" => "ILP");
     }
 

From 345f5b295cc6018bb64bb056900957d89921cdf5 Mon Sep 17 00:00:00 2001
From: GiggleLiu <cacate0129@gmail.com>
Date: Sat, 31 Jan 2026 11:32:03 +0800
Subject: [PATCH 6/9] fix: Make reduction graph JSON export deterministic
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Sort nodes by id and edges by (source, target) to ensure stable
output across runs. Regenerate reduction_graph.json with sorted data.

Addresses PR review comment #6.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 docs/paper/reduction_graph.json | 122 ++++++++++++++++----------------
 src/rules/graph.rs              |   9 ++-
 2 files changed, 67 insertions(+), 64 deletions(-)

diff --git a/docs/paper/reduction_graph.json b/docs/paper/reduction_graph.json
index 64c7407e9..cf26ef102 100644
--- a/docs/paper/reduction_graph.json
+++ b/docs/paper/reduction_graph.json
@@ -1,29 +1,24 @@
 {
   "nodes": [
     {
-      "id": "IndependentSet",
-      "label": "IndependentSet",
-      "category": "graph"
-    },
-    {
-      "id": "DominatingSet",
-      "label": "DominatingSet",
-      "category": "graph"
+      "id": "CircuitSAT",
+      "label": "CircuitSAT",
+      "category": "satisfiability"
     },
     {
-      "id": "VertexCovering",
-      "label": "VertexCovering",
+      "id": "Coloring",
+      "label": "Coloring",
       "category": "graph"
     },
     {
-      "id": "Matching",
-      "label": "Matching",
+      "id": "DominatingSet",
+      "label": "DominatingSet",
       "category": "graph"
     },
     {
-      "id": "Coloring",
-      "label": "Coloring",
-      "category": "graph"
+      "id": "Factoring",
+      "label": "Factoring",
+      "category": "specialized"
     },
     {
       "id": "ILP",
@@ -31,24 +26,19 @@
       "category": "optimization"
     },
     {
-      "id": "KSatisfiability",
-      "label": "KSatisfiability",
-      "category": "satisfiability"
+      "id": "IndependentSet",
+      "label": "IndependentSet",
+      "category": "graph"
     },
     {
-      "id": "CircuitSAT",
-      "label": "CircuitSAT",
+      "id": "KSatisfiability",
+      "label": "KSatisfiability",
       "category": "satisfiability"
     },
     {
-      "id": "SetCovering",
-      "label": "SetCovering",
-      "category": "set"
-    },
-    {
-      "id": "Factoring",
-      "label": "Factoring",
-      "category": "specialized"
+      "id": "Matching",
+      "label": "Matching",
+      "category": "graph"
     },
     {
       "id": "MaxCut",
@@ -56,60 +46,60 @@
       "category": "graph"
     },
     {
-      "id": "SpinGlass",
-      "label": "SpinGlass",
+      "id": "QUBO",
+      "label": "QUBO",
       "category": "optimization"
     },
+    {
+      "id": "Satisfiability",
+      "label": "Satisfiability",
+      "category": "satisfiability"
+    },
+    {
+      "id": "SetCovering",
+      "label": "SetCovering",
+      "category": "set"
+    },
     {
       "id": "SetPacking",
       "label": "SetPacking",
       "category": "set"
     },
     {
-      "id": "QUBO",
-      "label": "QUBO",
+      "id": "SpinGlass",
+      "label": "SpinGlass",
       "category": "optimization"
     },
     {
-      "id": "Satisfiability",
-      "label": "Satisfiability",
-      "category": "satisfiability"
+      "id": "VertexCovering",
+      "label": "VertexCovering",
+      "category": "graph"
     }
   ],
   "edges": [
     {
-      "source": "SetPacking",
-      "target": "IndependentSet",
-      "bidirectional": true
-    },
-    {
-      "source": "Satisfiability",
-      "target": "DominatingSet",
-      "bidirectional": false
-    },
-    {
-      "source": "Matching",
-      "target": "SetPacking",
+      "source": "CircuitSAT",
+      "target": "SpinGlass",
       "bidirectional": false
     },
     {
-      "source": "CircuitSAT",
-      "target": "SpinGlass",
+      "source": "Factoring",
+      "target": "CircuitSAT",
       "bidirectional": false
     },
     {
-      "source": "Satisfiability",
-      "target": "IndependentSet",
+      "source": "Factoring",
+      "target": "ILP",
       "bidirectional": false
     },
     {
-      "source": "SpinGlass",
-      "target": "QUBO",
+      "source": "KSatisfiability",
+      "target": "Satisfiability",
       "bidirectional": true
     },
     {
-      "source": "VertexCovering",
-      "target": "SetCovering",
+      "source": "Matching",
+      "target": "SetPacking",
       "bidirectional": false
     },
     {
@@ -118,29 +108,39 @@
       "bidirectional": false
     },
     {
-      "source": "Factoring",
-      "target": "ILP",
+      "source": "Satisfiability",
+      "target": "DominatingSet",
       "bidirectional": false
     },
     {
-      "source": "Factoring",
-      "target": "CircuitSAT",
+      "source": "Satisfiability",
+      "target": "IndependentSet",
       "bidirectional": false
     },
+    {
+      "source": "SetPacking",
+      "target": "IndependentSet",
+      "bidirectional": true
+    },
     {
       "source": "SpinGlass",
       "target": "MaxCut",
       "bidirectional": true
     },
     {
-      "source": "KSatisfiability",
-      "target": "Satisfiability",
+      "source": "SpinGlass",
+      "target": "QUBO",
       "bidirectional": true
     },
     {
       "source": "VertexCovering",
       "target": "IndependentSet",
       "bidirectional": true
+    },
+    {
+      "source": "VertexCovering",
+      "target": "SetCovering",
+      "bidirectional": false
     }
   ]
 }
\ No newline at end of file
diff --git a/src/rules/graph.rs b/src/rules/graph.rs
index 33a2366c6..fa69085eb 100644
--- a/src/rules/graph.rs
+++ b/src/rules/graph.rs
@@ -576,8 +576,8 @@ impl ReductionGraph {
             }
         }
 
-        // Build nodes with categories
-        let nodes: Vec<NodeJson> = self
+        // Build nodes with categories, sorted by id for deterministic output
+        let mut nodes: Vec<NodeJson> = self
             .name_indices
             .keys()
             .map(|&name| {
@@ -589,9 +589,11 @@ impl ReductionGraph {
                 }
             })
             .collect();
+        nodes.sort_by(|a, b| a.id.cmp(&b.id));
 
         // Build edges (only include one direction for bidirectional edges)
-        let edges: Vec<EdgeJson> = edge_set
+        // Sort by (source, target) for deterministic output
+        let mut edges: Vec<EdgeJson> = edge_set
             .into_iter()
             .map(|((src, dst), bidirectional)| EdgeJson {
                 source: src.to_string(),
@@ -599,6 +601,7 @@ impl ReductionGraph {
                 bidirectional,
             })
             .collect();
+        edges.sort_by(|a, b| (&a.source, &a.target).cmp(&(&b.source, &b.target)));
 
         ReductionGraphJson { nodes, edges }
     }

From 0f5f5437c8280896e11119ef4090c777058e162f Mon Sep 17 00:00:00 2001
From: GiggleLiu <cacate0129@gmail.com>
Date: Sat, 31 Jan 2026 11:40:49 +0800
Subject: [PATCH 7/9] =?UTF-8?q?refactor:=20Remove=20redundant=20add=5Fedge?=
 =?UTF-8?q?=20for=20Factoring=E2=86=92ILP?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

inventory::submit! already adds edges automatically. Manual add_edge!
is only needed for legacy reductions without inventory registration.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 src/rules/graph.rs | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/src/rules/graph.rs b/src/rules/graph.rs
index fa69085eb..ced109219 100644
--- a/src/rules/graph.rs
+++ b/src/rules/graph.rs
@@ -320,9 +320,8 @@ impl ReductionGraph {
         add_edge!("CircuitSAT" => "SpinGlass");
         add_edge!("Factoring" => "CircuitSAT");
 
-        // ILP reductions (for solver targets)
-        #[cfg(feature = "ilp")]
-        add_edge!("Factoring" => "ILP");
+        // Note: ILP reductions are auto-discovered via inventory::submit!
+        // No manual add_edge! needed for reductions with inventory registration.
     }
 
     /// Check if `sub` is a subtype of `sup` (or equal).

From dbbfb1375bf3f60bef1de4bfcfde746b1f119c56 Mon Sep 17 00:00:00 2001
From: GiggleLiu <cacate0129@gmail.com>
Date: Sat, 31 Jan 2026 11:42:59 +0800
Subject: [PATCH 8/9] refactor: Remove all redundant manual add_edge! calls
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

All reductions now use inventory::submit! for auto-discovery.
The register_reductions function is kept as a template for future
manual registrations if needed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 src/rules/graph.rs | 70 +++++++++++++---------------------------------
 1 file changed, 20 insertions(+), 50 deletions(-)

diff --git a/src/rules/graph.rs b/src/rules/graph.rs
index ced109219..4edfa63e3 100644
--- a/src/rules/graph.rs
+++ b/src/rules/graph.rs
@@ -270,58 +270,28 @@ impl ReductionGraph {
         }
     }
 
+    #[allow(unused)]
     fn register_reductions(
-        graph: &mut DiGraph<&'static str, ReductionEdge>,
-        name_indices: &HashMap<&'static str, NodeIndex>,
+        _graph: &mut DiGraph<&'static str, ReductionEdge>,
+        _name_indices: &HashMap<&'static str, NodeIndex>,
     ) {
-        // Add an edge between two problem types by name (with default overhead).
-        // This is for backward compatibility with manually registered reductions.
-        macro_rules! add_edge {
-            ($src:expr => $dst:expr) => {
-                if let (Some(&src), Some(&dst)) = (name_indices.get($src), name_indices.get($dst)) {
-                    // Avoid duplicate edges
-                    if graph.find_edge(src, dst).is_none() {
-                        graph.add_edge(
-                            src,
-                            dst,
-                            ReductionEdge {
-                                source_graph: "SimpleGraph",
-                                target_graph: "SimpleGraph",
-                                overhead: ReductionOverhead::default(),
-                            },
-                        );
-                    }
-                }
-            };
-        }
-
-        // Graph problem reductions
-        add_edge!("IndependentSet" => "VertexCovering");
-        add_edge!("VertexCovering" => "IndependentSet");
-        add_edge!("IndependentSet" => "SetPacking");
-        add_edge!("SetPacking" => "IndependentSet");
-        add_edge!("VertexCovering" => "SetCovering");
-        add_edge!("Matching" => "SetPacking");
-
-        // Optimization reductions
-        add_edge!("SpinGlass" => "QUBO");
-        add_edge!("QUBO" => "SpinGlass");
-        add_edge!("MaxCut" => "SpinGlass");
-        add_edge!("SpinGlass" => "MaxCut");
-
-        // SAT-based reductions
-        add_edge!("Satisfiability" => "KSatisfiability");
-        add_edge!("KSatisfiability" => "Satisfiability");
-        add_edge!("Satisfiability" => "IndependentSet");
-        add_edge!("Satisfiability" => "Coloring");
-        add_edge!("Satisfiability" => "DominatingSet");
-
-        // Circuit reductions
-        add_edge!("CircuitSAT" => "SpinGlass");
-        add_edge!("Factoring" => "CircuitSAT");
-
-        // Note: ILP reductions are auto-discovered via inventory::submit!
-        // No manual add_edge! needed for reductions with inventory registration.
+        // All reductions are now auto-discovered via inventory::submit!
+        // This function is kept for potential future manual registrations
+        // that don't use the inventory pattern.
+        //
+        // Example usage:
+        // ```
+        // macro_rules! add_edge {
+        //     ($src:expr => $dst:expr) => {
+        //         if let (Some(&src), Some(&dst)) = (_name_indices.get($src), _name_indices.get($dst)) {
+        //             if _graph.find_edge(src, dst).is_none() {
+        //                 _graph.add_edge(src, dst, ReductionEdge::default());
+        //             }
+        //         }
+        //     };
+        // }
+        // add_edge!("SourceProblem" => "TargetProblem");
+        // ```
     }
 
     /// Check if `sub` is a subtype of `sup` (or equal).

From 523666e2e0925e6b4450c73da3ad88a3c0d64f0f Mon Sep 17 00:00:00 2001
From: GiggleLiu <cacate0129@gmail.com>
Date: Sat, 31 Jan 2026 11:43:55 +0800
Subject: [PATCH 9/9] refactor: Remove empty register_reductions function
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

All reductions use inventory::submit! for auto-discovery.
No need for manual registration function.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 src/rules/graph.rs | 27 ---------------------------
 1 file changed, 27 deletions(-)

diff --git a/src/rules/graph.rs b/src/rules/graph.rs
index 4edfa63e3..2cb09ce98 100644
--- a/src/rules/graph.rs
+++ b/src/rules/graph.rs
@@ -162,9 +162,6 @@ impl ReductionGraph {
             }
         }
 
-        // Also register manual reductions for backward compatibility
-        Self::register_reductions(&mut graph, &name_indices);
-
         Self {
             graph,
             name_indices,
@@ -270,30 +267,6 @@ impl ReductionGraph {
         }
     }
 
-    #[allow(unused)]
-    fn register_reductions(
-        _graph: &mut DiGraph<&'static str, ReductionEdge>,
-        _name_indices: &HashMap<&'static str, NodeIndex>,
-    ) {
-        // All reductions are now auto-discovered via inventory::submit!
-        // This function is kept for potential future manual registrations
-        // that don't use the inventory pattern.
-        //
-        // Example usage:
-        // ```
-        // macro_rules! add_edge {
-        //     ($src:expr => $dst:expr) => {
-        //         if let (Some(&src), Some(&dst)) = (_name_indices.get($src), _name_indices.get($dst)) {
-        //             if _graph.find_edge(src, dst).is_none() {
-        //                 _graph.add_edge(src, dst, ReductionEdge::default());
-        //             }
-        //         }
-        //     };
-        // }
-        // add_edge!("SourceProblem" => "TargetProblem");
-        // ```
-    }
-
     /// Check if `sub` is a subtype of `sup` (or equal).
     pub fn is_graph_subtype(&self, sub: &str, sup: &str) -> bool {
         sub == sup
