problemreductions/rules/
factoring_ilp.rs

1//! Reduction from Factoring to ILP (Integer Linear Programming).
2//!
3//! The Integer Factoring problem can be formulated as an ILP using
4//! McCormick linearization for binary products combined with carry propagation.
5//!
6//! Given target N and bit widths m, n, find factors p (m bits) and q (n bits)
7//! such that p × q = N.
8//!
9//! ## Variables
10//! - `p_i ∈ {0,1}` for i = 0..m-1 (first factor bits)
11//! - `q_j ∈ {0,1}` for j = 0..n-1 (second factor bits)
12//! - `z_ij ∈ {0,1}` for each (i,j) pair (product p_i × q_j)
13//! - `c_k ∈ ℤ≥0` for k = 0..m+n-1 (carry at each bit position)
14//!
15//! ## Constraints
16//! 1. Product linearization (McCormick): z_ij ≤ p_i, z_ij ≤ q_j, z_ij ≥ p_i + q_j - 1
17//! 2. Bit-position sums: Σ_{i+j=k} z_ij + c_{k-1} = N_k + 2·c_k
18//! 3. No overflow: c_{m+n-1} = 0
19//! 4. Binary bounds: p_i ≤ 1, q_j ≤ 1
20//! 5. Carry bounds: 0 ≤ c_k ≤ min(m, n)
21
22use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP};
23use crate::models::misc::Factoring;
24use crate::reduction;
25use crate::rules::traits::{ReduceTo, ReductionResult};
26use std::cmp::min;
27
28/// Result of reducing Factoring to ILP.
29///
30/// This reduction creates an ILP where:
31/// - Binary variables represent factor bits and their products
32/// - Integer variables represent carries at each bit position
33/// - Constraints enforce the multiplication equals the target
34#[derive(Debug, Clone)]
35pub struct ReductionFactoringToILP {
36    target: ILP<i32>,
37    m: usize, // bits for first factor
38    n: usize, // bits for second factor
39}
40
41impl ReductionFactoringToILP {
42    /// Get the variable index for p_i (first factor bit i).
43    fn p_var(&self, i: usize) -> usize {
44        i
45    }
46
47    /// Get the variable index for q_j (second factor bit j).
48    fn q_var(&self, j: usize) -> usize {
49        self.m + j
50    }
51
52    /// Get the variable index for z_ij (product p_i × q_j).
53    #[cfg(test)]
54    fn z_var(&self, i: usize, j: usize) -> usize {
55        self.m + self.n + i * self.n + j
56    }
57
58    /// Get the variable index for carry at position k.
59    #[cfg(test)]
60    fn carry_var(&self, k: usize) -> usize {
61        self.m + self.n + self.m * self.n + k
62    }
63}
64
65impl ReductionResult for ReductionFactoringToILP {
66    type Source = Factoring;
67    type Target = ILP<i32>;
68
69    fn target_problem(&self) -> &ILP<i32> {
70        &self.target
71    }
72
73    /// Extract solution from ILP back to Factoring.
74    ///
75    /// The first m variables are p_i (first factor bits).
76    /// The next n variables are q_j (second factor bits).
77    /// Returns concatenated bit vector [p_0, ..., p_{m-1}, q_0, ..., q_{n-1}].
78    fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
79        // Extract p bits (first factor)
80        let p_bits: Vec<usize> = (0..self.m)
81            .map(|i| target_solution.get(self.p_var(i)).copied().unwrap_or(0))
82            .collect();
83
84        // Extract q bits (second factor)
85        let q_bits: Vec<usize> = (0..self.n)
86            .map(|j| target_solution.get(self.q_var(j)).copied().unwrap_or(0))
87            .collect();
88
89        // Concatenate p and q bits
90        let mut result = p_bits;
91        result.extend(q_bits);
92        result
93    }
94}
95
96#[reduction(overhead = {
97    num_vars = "num_bits_first * num_bits_second",
98    num_constraints = "num_bits_first * num_bits_second",
99})]
100impl ReduceTo<ILP<i32>> for Factoring {
101    type Result = ReductionFactoringToILP;
102
103    fn reduce_to(&self) -> Self::Result {
104        let m = self.m();
105        let n = self.n();
106        let target = self.target();
107
108        // Calculate the number of bits needed for the target
109        let target_bits = if target == 0 {
110            1
111        } else {
112            (64 - target.leading_zeros()) as usize
113        };
114
115        // Number of bit positions to check: max(m+n, target_bits)
116        // For feasible instances, target_bits <= m+n (product of m-bit × n-bit has at most m+n bits).
117        // When target_bits > m+n, the ILP will be infeasible (target too large for given bit widths).
118        // Using max() here ensures proper infeasibility detection through the bit equations.
119        let num_bit_positions = std::cmp::max(m + n, target_bits);
120
121        // Total variables: m + n + m*n + num_bit_positions
122        let num_p = m;
123        let num_q = n;
124        let num_z = m * n;
125        let num_carries = num_bit_positions;
126        let num_vars = num_p + num_q + num_z + num_carries;
127
128        // Helper functions for variable indices
129        let p_var = |i: usize| -> usize { i };
130        let q_var = |j: usize| -> usize { m + j };
131        let z_var = |i: usize, j: usize| -> usize { m + n + i * n + j };
132        let carry_var = |k: usize| -> usize { m + n + m * n + k };
133
134        let mut constraints = Vec::new();
135
136        // Constraint 1: Product linearization (McCormick constraints)
137        // For each z_ij = p_i × q_j:
138        //   z_ij ≤ p_i
139        //   z_ij ≤ q_j
140        //   z_ij ≥ p_i + q_j - 1
141        for i in 0..m {
142            for j in 0..n {
143                let z = z_var(i, j);
144                let p = p_var(i);
145                let q = q_var(j);
146
147                // z_ij - p_i ≤ 0
148                constraints.push(LinearConstraint::le(vec![(z, 1.0), (p, -1.0)], 0.0));
149
150                // z_ij - q_j ≤ 0
151                constraints.push(LinearConstraint::le(vec![(z, 1.0), (q, -1.0)], 0.0));
152
153                // z_ij - p_i - q_j ≥ -1
154                constraints.push(LinearConstraint::ge(
155                    vec![(z, 1.0), (p, -1.0), (q, -1.0)],
156                    -1.0,
157                ));
158            }
159        }
160
161        // Constraint 2: Bit-position equations
162        // For each bit position k = 0..num_bit_positions-1:
163        //   Σ_{i+j=k} z_ij + c_{k-1} = N_k + 2·c_k
164        // Rearranged: Σ_{i+j=k} z_ij + c_{k-1} - 2·c_k = N_k
165        for k in 0..num_bit_positions {
166            let mut terms: Vec<(usize, f64)> = Vec::new();
167
168            // Collect all z_ij where i + j = k
169            for i in 0..m {
170                if k >= i && k - i < n {
171                    let j = k - i;
172                    terms.push((z_var(i, j), 1.0));
173                }
174            }
175
176            // Add carry_in (from position k-1)
177            if k > 0 {
178                terms.push((carry_var(k - 1), 1.0));
179            }
180
181            // Subtract 2 × carry_out
182            terms.push((carry_var(k), -2.0));
183
184            // RHS is N_k (k-th bit of target). For k >= 64, the bit is 0 for u64.
185            let n_k = if k < 64 {
186                ((target >> k) & 1) as f64
187            } else {
188                0.0
189            };
190            constraints.push(LinearConstraint::eq(terms, n_k));
191        }
192
193        // Constraint 3: Final carry must be zero (no overflow)
194        constraints.push(LinearConstraint::eq(
195            vec![(carry_var(num_bit_positions - 1), 1.0)],
196            0.0,
197        ));
198
199        // Constraint 4: Binary bounds for p_i and q_j (enforce 0/1 in integer domain)
200        for i in 0..m {
201            constraints.push(LinearConstraint::le(vec![(p_var(i), 1.0)], 1.0));
202        }
203        for j in 0..n {
204            constraints.push(LinearConstraint::le(vec![(q_var(j), 1.0)], 1.0));
205        }
206
207        // Constraint 5: Carry bounds (0 ≤ c_k ≤ min(m, n))
208        let carry_upper = min(m, n) as f64;
209        for k in 0..num_carries {
210            let cv = carry_var(k);
211            constraints.push(LinearConstraint::ge(vec![(cv, 1.0)], 0.0));
212            constraints.push(LinearConstraint::le(vec![(cv, 1.0)], carry_upper));
213        }
214
215        // Objective: feasibility problem (minimize 0)
216        let objective: Vec<(usize, f64)> = vec![];
217
218        let ilp = ILP::<i32>::new(num_vars, constraints, objective, ObjectiveSense::Minimize);
219
220        ReductionFactoringToILP { target: ilp, m, n }
221    }
222}
223
224#[cfg(test)]
225#[path = "../unit_tests/rules/factoring_ilp.rs"]
226mod tests;