Skip to main content

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::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP};
23use crate::models::misc::Factoring;
24use crate::reduction;
25use crate::rules::ilp_helpers::mccormick_product;
26use crate::rules::traits::{ReduceTo, ReductionResult};
27use std::cmp::min;
28
29/// Result of reducing Factoring to ILP.
30///
31/// This reduction creates an ILP where:
32/// - Binary variables represent factor bits and their products
33/// - Integer variables represent carries at each bit position
34/// - Constraints enforce the multiplication equals the target
35#[derive(Debug, Clone)]
36pub struct ReductionFactoringToILP {
37    target: ILP<i64, i64, Bounded>,
38    m: usize, // bits for first factor
39    n: usize, // bits for second factor
40}
41
42impl ReductionFactoringToILP {
43    /// Get the variable index for p_i (first factor bit i).
44    fn p_var(&self, i: usize) -> usize {
45        i
46    }
47
48    /// Get the variable index for q_j (second factor bit j).
49    fn q_var(&self, j: usize) -> usize {
50        self.m + j
51    }
52
53    /// Get the variable index for z_ij (product p_i × q_j).
54    #[cfg(test)]
55    fn z_var(&self, i: usize, j: usize) -> usize {
56        self.m + self.n + i * self.n + j
57    }
58
59    /// Get the variable index for carry at position k.
60    #[cfg(test)]
61    fn carry_var(&self, k: usize) -> usize {
62        self.m + self.n + self.m * self.n + k
63    }
64}
65
66impl ReductionResult for ReductionFactoringToILP {
67    type Source = Factoring;
68    type Target = ILP<i64, i64, Bounded>;
69
70    fn target_problem(&self) -> &ILP<i64, i64, Bounded> {
71        &self.target
72    }
73
74    /// Extract solution from ILP back to Factoring.
75    ///
76    /// The first m variables are p_i (first factor bits).
77    /// The next n variables are q_j (second factor bits).
78    /// Returns the decoded factors in ascending order.
79    fn extract_solution(
80        &self,
81        target_solution: &<Self::Target as crate::traits::Problem>::Solution,
82    ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
83        crate::rules::traits::validate_target_witness(
84            self.target_problem(),
85            target_solution,
86            |value| value.value.is_some(),
87            "target ILP assignment is infeasible",
88        )?;
89
90        Ok({
91            // Extract p bits (first factor)
92            let p = (0..self.m)
93                .filter(|&i| target_solution[self.p_var(i)] == 1)
94                .fold(num_bigint::BigUint::from(0u8), |value, index| {
95                    value + (num_bigint::BigUint::from(1u8) << index)
96                });
97
98            // Extract q bits (second factor)
99            let q = (0..self.n)
100                .filter(|&j| target_solution[self.q_var(j)] == 1)
101                .fold(num_bigint::BigUint::from(0u8), |value, index| {
102                    value + (num_bigint::BigUint::from(1u8) << index)
103                });
104            if p <= q {
105                (p, q)
106            } else {
107                (q, p)
108            }
109        })
110    }
111}
112
113#[crate::aggregate_reduction(ilp_feasibility)]
114impl crate::rules::AggregateReductionResult for ReductionFactoringToILP {}
115
116// With L=max(m+n,target_bits), McCormick rows use 7mn terms; bit equations,
117// final carry, factor bounds and carry bounds use mn+m+n+4L more.
118// Substitute L <= m+n+target_bits to obtain a bound using source parameters.
119#[reduction(transform = upper_bound {
120    max_constraint_magnitude_bits = "num_bits_first + num_bits_second + 2",
121    num_vars = "num_bits_first * num_bits_second + 2 * num_bits_first + 2 * num_bits_second + target_bits",
122    num_constraints = "3 * num_bits_first * num_bits_second + 4 * num_bits_first + 4 * num_bits_second + 3 * target_bits + 1",
123    num_nonzeros = "8 * num_bits_first * num_bits_second + 5 * num_bits_first + 5 * num_bits_second + 4 * target_bits",
124})]
125impl ReduceTo<ILP<i64, i64, Bounded>> for Factoring {
126    type Result = ReductionFactoringToILP;
127
128    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
129        let m = self.m();
130        let n = self.n();
131        let target = self.target();
132
133        // Calculate the number of bits needed for the target
134        let target_bits = self.target_bits();
135
136        // Number of bit positions to check: max(m+n, target_bits)
137        // For feasible instances, target_bits <= m+n (product of m-bit × n-bit has at most m+n bits).
138        // When target_bits > m+n, the ILP will be infeasible (target too large for given bit widths).
139        // Using max() here ensures proper infeasibility detection through the bit equations.
140        let num_bit_positions = std::cmp::max(m + n, target_bits);
141
142        // Total variables: m + n + m*n + num_bit_positions
143        let num_p = m;
144        let num_q = n;
145        let num_z = m * n;
146        let num_carries = num_bit_positions;
147        let num_vars = num_p + num_q + num_z + num_carries;
148
149        // Helper functions for variable indices
150        let p_var = |i: usize| -> usize { i };
151        let q_var = |j: usize| -> usize { m + j };
152        let z_var = |i: usize, j: usize| -> usize { m + n + i * n + j };
153        let carry_var = |k: usize| -> usize { m + n + m * n + k };
154
155        let mut constraints = Vec::new();
156
157        // Constraint 1: Product linearization (McCormick constraints)
158        // For each z_ij = p_i × q_j:
159        //   z_ij ≤ p_i
160        //   z_ij ≤ q_j
161        //   z_ij ≥ p_i + q_j - 1
162        for i in 0..m {
163            for j in 0..n {
164                let z = z_var(i, j);
165                let p = p_var(i);
166                let q = q_var(j);
167
168                constraints.extend(mccormick_product(z, p, q));
169            }
170        }
171
172        // Constraint 2: Bit-position equations
173        // For each bit position k = 0..num_bit_positions-1:
174        //   Σ_{i+j=k} z_ij + c_{k-1} = N_k + 2·c_k
175        // Rearranged: Σ_{i+j=k} z_ij + c_{k-1} - 2·c_k = N_k
176        for k in 0..num_bit_positions {
177            let mut terms: Vec<(usize, i64)> = Vec::new();
178
179            // Collect all z_ij where i + j = k
180            for i in 0..m {
181                if k >= i && k - i < n {
182                    let j = k - i;
183                    terms.push((z_var(i, j), 1));
184                }
185            }
186
187            // Add carry_in (from position k-1)
188            if k > 0 {
189                terms.push((carry_var(k - 1), 1));
190            }
191
192            // Subtract 2 × carry_out
193            terms.push((carry_var(k), -2));
194
195            // RHS is N_k (k-th bit of target).
196            let n_k = i64::from(target.bit(u64::try_from(k).expect("bit index fits u64")));
197            constraints.push(LinearConstraint::eq(terms, n_k));
198        }
199
200        // Constraint 3: Final carry must be zero (no overflow)
201        constraints.push(LinearConstraint::eq(
202            vec![(carry_var(num_bit_positions - 1), 1)],
203            0,
204        ));
205
206        // Constraint 4: Binary bounds for p_i and q_j (enforce 0/1 in integer domain)
207        for i in 0..m {
208            constraints.push(LinearConstraint::le(vec![(p_var(i), 1)], 1));
209        }
210        for j in 0..n {
211            constraints.push(LinearConstraint::le(vec![(q_var(j), 1)], 1));
212        }
213
214        // Constraint 5: Carry bounds (0 ≤ c_k ≤ min(m, n))
215        let carry_upper = <Self as ReduceTo<ILP<i64, i64, Bounded>>>::exact_i64(
216            min(m, n),
217            "encoding a carry bound",
218        )?;
219        for k in 0..num_carries {
220            let cv = carry_var(k);
221            constraints.push(LinearConstraint::ge(vec![(cv, 1)], 0));
222            constraints.push(LinearConstraint::le(vec![(cv, 1)], carry_upper));
223        }
224
225        // Objective: feasibility problem (minimize 0)
226        let objective: Vec<(usize, i64)> = vec![];
227
228        let mut variables = vec![IntegerVariable::binary(); num_vars];
229        variables[num_p + num_q + num_z..].fill(
230            IntegerVariable::new(Some(0), Some(carry_upper))
231                .map_err(<Self as ReduceTo<ILP<i64, i64, Bounded>>>::target_construction)?,
232        );
233
234        let ilp = ILP::<i64, i64, Bounded>::with_variables(
235            variables,
236            constraints,
237            objective,
238            ObjectiveSense::Minimize,
239        )
240        .map_err(<Self as ReduceTo<ILP<i64, i64, Bounded>>>::target_construction)?;
241
242        Ok(ReductionFactoringToILP { target: ilp, m, n })
243    }
244}
245
246#[cfg(feature = "example-db")]
247pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
248    vec![crate::example_db::specs::RuleExampleSpec {
249        id: "factoring_to_ilp",
250        build: || {
251            let source = Factoring::with_factor_bits(35, 3, 3);
252            crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source)
253        },
254    }]
255}
256
257#[cfg(test)]
258#[path = "../unit_tests/rules/factoring_ilp.rs"]
259mod tests;