Skip to main content

problemreductions/rules/
factoring_circuit.rs

1//! Reduction from Factoring to CircuitSAT.
2//!
3//! The reduction constructs a multiplier circuit that computes p × q
4//! and constrains the output to equal the target number N.
5//! A satisfying assignment to the circuit gives the factorization.
6//!
7//! The multiplier circuit uses an array multiplier structure with
8//! carry propagation, building up partial products row by row.
9
10use crate::models::formula::{Assignment, BooleanExpr, Circuit, CircuitSAT};
11use crate::models::misc::Factoring;
12use crate::reduction;
13use crate::rules::traits::{ReduceTo, ReductionResult};
14use num_bigint::BigUint;
15use num_traits::{One, Zero};
16/// Result of reducing Factoring to CircuitSAT.
17///
18/// This struct contains:
19/// - The target CircuitSAT problem (the multiplier circuit)
20/// - Variable indices for the first factor p (m bits)
21/// - Variable indices for the second factor q (n bits)
22/// - Variable indices for the product m (m+n bits)
23#[derive(Debug, Clone)]
24pub struct ReductionFactoringToCircuit {
25    /// The target CircuitSAT problem.
26    target: CircuitSAT,
27    /// Variable names for the first factor p (bit positions).
28    p_vars: Vec<String>,
29    /// Variable names for the second factor q (bit positions).
30    q_vars: Vec<String>,
31    /// Variable names for the product (bit positions).
32    m_vars: Vec<String>,
33}
34
35impl ReductionResult for ReductionFactoringToCircuit {
36    type Source = Factoring;
37    type Target = CircuitSAT;
38
39    fn target_problem(&self) -> &Self::Target {
40        &self.target
41    }
42
43    /// Extract a Factoring solution from a CircuitSAT solution.
44    ///
45    /// Returns the decoded factors in ascending order.
46    fn extract_solution(
47        &self,
48        target_solution: &<Self::Target as crate::traits::Problem>::Solution,
49    ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
50        crate::rules::traits::validate_target_witness(
51            self.target_problem(),
52            target_solution,
53            |value| value.0,
54            "target assignment does not satisfy the multiplication circuit",
55        )?;
56
57        Ok({
58            let var_names = self.target.variable_names();
59
60            // Build a map from variable name to its value
61            let var_map: std::collections::HashMap<&str, bool> = var_names
62                .iter()
63                .enumerate()
64                .map(|(i, name)| (name.as_str(), target_solution[i]))
65                .collect();
66
67            let decode = |names: &[String]| {
68                names
69                    .iter()
70                    .enumerate()
71                    .try_fold(BigUint::zero(), |value, (index, name)| {
72                        let bit = var_map.get(name.as_str()).copied().ok_or_else(|| {
73                            crate::rules::ExtractionError::invalid(format!(
74                                "target circuit does not contain factor variable {name}"
75                            ))
76                        })?;
77                        Ok::<BigUint, crate::rules::ExtractionError>(if bit {
78                            value + (BigUint::one() << index)
79                        } else {
80                            value
81                        })
82                    })
83            };
84            let left = decode(&self.p_vars)?;
85            let right = decode(&self.q_vars)?;
86            if left <= right {
87                (left, right)
88            } else {
89                (right, left)
90            }
91        })
92    }
93}
94
95impl ReductionFactoringToCircuit {
96    /// Product width and assignment capacity, checked before circuit allocation.
97    fn dimensions(m: usize, n: usize) -> Result<(usize, usize), crate::rules::ReductionError> {
98        let overflow = || {
99            crate::rules::ReductionError::integer_overflow::<Factoring, CircuitSAT>(
100                "computing multiplication circuit dimensions",
101            )
102        };
103        let width = m.checked_add(n).ok_or_else(overflow)?;
104        let capacity = m
105            .checked_mul(n)
106            .and_then(|v| v.checked_mul(6))
107            .and_then(|v| width.checked_mul(2).and_then(|w| v.checked_add(w)))
108            .and_then(|v| v.checked_add(2))
109            .ok_or_else(overflow)?;
110        Ok((width, capacity))
111    }
112
113    /// Get the variable names for the first factor.
114    pub fn p_vars(&self) -> &[String] {
115        &self.p_vars
116    }
117
118    /// Get the variable names for the second factor.
119    pub fn q_vars(&self) -> &[String] {
120        &self.q_vars
121    }
122
123    /// Get the variable names for the product.
124    pub fn m_vars(&self) -> &[String] {
125        &self.m_vars
126    }
127}
128
129/// Read the i-th bit (1-indexed) of a number (little-endian).
130fn read_bit(n: &BigUint, i: usize) -> bool {
131    if i == 0 {
132        false
133    } else {
134        n.bit(u64::try_from(i - 1).expect("bit index fits u64"))
135    }
136}
137
138/// Build a single multiplier cell that computes:
139/// s + 2*c = p*q + s_pre + c_pre
140///
141/// This is a full adder that adds three bits: (p AND q), s_pre, and c_pre.
142/// Returns the assignments needed and the list of ancilla variable names.
143fn build_multiplier_cell(
144    s_name: &str,
145    c_name: &str,
146    p_name: &str,
147    q_name: &str,
148    s_pre: &BooleanExpr,
149    c_pre: &BooleanExpr,
150    cell_id: &str,
151) -> (Vec<Assignment>, Vec<String>) {
152    // Create unique ancilla variable names
153    let a_name = format!("a_{}", cell_id);
154    let a_xor_s_name = format!("axs_{}", cell_id);
155    let a_xor_s_and_c_name = format!("axsc_{}", cell_id);
156    let a_and_s_name = format!("as_{}", cell_id);
157
158    let p = BooleanExpr::var(p_name);
159    let q = BooleanExpr::var(q_name);
160    let a = BooleanExpr::var(&a_name);
161    let a_xor_s = BooleanExpr::var(&a_xor_s_name);
162
163    // Build the assignments:
164    // a = p & q (AND of the two factor bits)
165    let assign_a = Assignment::new(vec![a_name.clone()], BooleanExpr::and(vec![p, q]));
166
167    // a_xor_s = a XOR s_pre
168    let assign_a_xor_s = Assignment::new(
169        vec![a_xor_s_name.clone()],
170        BooleanExpr::xor(vec![a.clone(), s_pre.clone()]),
171    );
172
173    // s = a_xor_s XOR c_pre (sum output)
174    let assign_s = Assignment::new(
175        vec![s_name.to_string()],
176        BooleanExpr::xor(vec![a_xor_s.clone(), c_pre.clone()]),
177    );
178
179    // a_xor_s_and_c = a_xor_s & c_pre
180    let assign_a_xor_s_and_c = Assignment::new(
181        vec![a_xor_s_and_c_name.clone()],
182        BooleanExpr::and(vec![a_xor_s, c_pre.clone()]),
183    );
184
185    // a_and_s = a & s_pre
186    let assign_a_and_s = Assignment::new(
187        vec![a_and_s_name.clone()],
188        BooleanExpr::and(vec![a, s_pre.clone()]),
189    );
190
191    // c = a_xor_s_and_c | a_and_s (carry output)
192    let assign_c = Assignment::new(
193        vec![c_name.to_string()],
194        BooleanExpr::or(vec![
195            BooleanExpr::var(&a_xor_s_and_c_name),
196            BooleanExpr::var(&a_and_s_name),
197        ]),
198    );
199
200    let assignments = vec![
201        assign_a,
202        assign_a_xor_s,
203        assign_s,
204        assign_a_xor_s_and_c,
205        assign_a_and_s,
206        assign_c,
207    ];
208
209    let ancillas = vec![a_name, a_xor_s_name, a_xor_s_and_c_name, a_and_s_name];
210
211    (assignments, ancillas)
212}
213
214#[crate::aggregate_reduction(identity)]
215impl crate::rules::AggregateReductionResult for ReductionFactoringToCircuit {}
216
217#[reduction(transform = upper_bound {
218    num_variables = "6 * num_bits_first * num_bits_second + 2 * (num_bits_first + num_bits_second) + 1",
219    num_assignments = "6 * num_bits_first * num_bits_second + 2 * (num_bits_first + num_bits_second) + 2",
220    num_assignment_outputs = "6 * num_bits_first * num_bits_second + 2 * (num_bits_first + num_bits_second) + 2",
221    num_expression_nodes = "5 * (6 * num_bits_first * num_bits_second + 2 * (num_bits_first + num_bits_second) + 2)",
222})]
223impl ReduceTo<CircuitSAT> for Factoring {
224    type Result = ReductionFactoringToCircuit;
225
226    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
227        let n1 = self.m(); // bits for first factor
228        let n2 = self.n(); // bits for second factor
229        let target = self.target();
230        let (width, capacity) = ReductionFactoringToCircuit::dimensions(n1, n2)?;
231
232        // Create input variables for the two factors
233        let p_vars: Vec<String> = (1..=n1).map(|i| format!("p{}", i)).collect();
234        let q_vars: Vec<String> = (1..=n2).map(|i| format!("q{}", i)).collect();
235
236        // Accumulate assignments and product bits
237        let mut assignments = Vec::with_capacity(capacity);
238        let mut product_bits = Vec::with_capacity(width);
239
240        // Initialize s_pre (previous sum signals) with false constants
241        // s_pre has n2+1 elements to handle the carry propagation
242        let mut s_pre = Vec::with_capacity(n2 + 1);
243        s_pre.push(BooleanExpr::constant(false));
244        // The zero accumulator is expressed as Q AND zero. This also keeps
245        // every Q input in the circuit when no multiplier rows are present.
246        s_pre.extend(q_vars.iter().map(|name| {
247            BooleanExpr::and(vec![BooleanExpr::var(name), BooleanExpr::constant(false)])
248        }));
249
250        // Build the array multiplier row by row
251        for i in 1..=n1 {
252            // c_pre is the carry from the previous cell in this row
253            let mut c_pre = BooleanExpr::constant(false);
254
255            for j in 1..=n2 {
256                // Create signal names for this cell
257                let c_name = format!("c{}_{}", i, j);
258                let s_name = format!("s{}_{}", i, j);
259
260                // Build the multiplier cell
261                let cell_id = format!("{}_{}", i, j);
262                let (cell_assignments, _ancillas) = build_multiplier_cell(
263                    &s_name,
264                    &c_name,
265                    &p_vars[i - 1],
266                    &q_vars[j - 1],
267                    &s_pre[j], // s_pre[j+1] in 0-indexed Julia becomes s_pre[j] in 1-indexed
268                    &c_pre,
269                    &cell_id,
270                );
271
272                assignments.extend(cell_assignments);
273
274                // Update c_pre for the next cell
275                c_pre = BooleanExpr::var(&c_name);
276
277                // Update s_pre for the next row
278                // s_pre[j-1] (0-indexed) = s (the sum from this cell)
279                s_pre[j - 1] = BooleanExpr::var(&s_name);
280            }
281
282            // The final carry becomes the last element of s_pre
283            s_pre[n2] = c_pre;
284
285            // The first element of s_pre is the i-th bit of the product
286            product_bits.push(s_pre[0].clone());
287        }
288
289        // After all rows, the residual accumulator supplies the remaining
290        // high bits. With zero rows these are the actual zero expressions,
291        // not names of multiplier cells that were never constructed.
292        product_bits.extend(s_pre.into_iter().skip(1));
293        let m_vars: Vec<_> = (0..width).map(|i| format!("product_{i}")).collect();
294        for (name, expr) in m_vars.iter().zip(product_bits) {
295            assignments.push(Assignment::new(vec![name.clone()], expr));
296        }
297
298        // Constrain the output bits to match the target number
299        for (i, m_var) in m_vars.iter().enumerate() {
300            let target_bit = read_bit(target, i + 1);
301            assignments.push(Assignment::new(
302                vec![m_var.clone()],
303                BooleanExpr::constant(target_bit),
304            ));
305        }
306
307        // An m-bit by n-bit product cannot contain a set bit above position m+n-1.
308        // Encode an explicit contradiction instead of truncating an oversized target.
309        if target.bits() > u64::try_from(m_vars.len()).expect("product width fits u64") {
310            let overflow = "target_overflow".to_string();
311            assignments.push(Assignment::new(
312                vec![overflow.clone()],
313                BooleanExpr::constant(false),
314            ));
315            assignments.push(Assignment::new(vec![overflow], BooleanExpr::constant(true)));
316        }
317
318        // Build the circuit
319        let circuit = Circuit::new(assignments);
320        let circuit_sat = CircuitSAT::new(circuit);
321
322        Ok(ReductionFactoringToCircuit {
323            target: circuit_sat,
324            p_vars,
325            q_vars,
326            m_vars,
327        })
328    }
329}
330
331#[cfg(feature = "example-db")]
332pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
333    use crate::export::SolutionPair;
334
335    vec![crate::example_db::specs::RuleExampleSpec {
336        id: "factoring_to_circuitsat",
337        build: || {
338            crate::example_db::specs::rule_example_with_witness::<_, CircuitSAT>(
339                Factoring::with_factor_bits(35, 3, 3),
340                SolutionPair {
341                    source_config: serde_json::to_value((BigUint::from(5u32), BigUint::from(7u32)))
342                        .expect("solution serialization must succeed"),
343                    target_config: serde_json::json!(vec![
344                        true, true, true, false, false, false, true, true, true, false, false,
345                        false, false, false, false, true, false, false, true, true, true, true,
346                        true, false, false, true, true, false, false, false, false, false, false,
347                        false, true, true, false, false, false, false, false, false, true, true,
348                        true, true, false, true, true, true, false, false, false, true, true, true,
349                        true, true, true, true, true, true, false, false, false, false
350                    ]),
351                },
352            )
353        },
354    }]
355}
356
357#[cfg(test)]
358#[path = "../unit_tests/rules/factoring_circuit.rs"]
359mod tests;