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;