Skip to main content

ries_rs/
pslq.rs

1//! PSLQ Integer Relation Algorithm
2//!
3//! This module implements the PSLQ (Partial Sums LQ) algorithm for finding
4//! integer relations between real numbers. PSLQ can discover identities like:
5//! - π ≈ 355/113 (rational approximation)
6//! - e^π - π ≈ 20 (near-integer relation)
7//! - 4π² ≈ 39.4784... (relation with π²)
8//!
9//! # References
10//!
11
12#![allow(clippy::needless_range_loop)]
13#![allow(dead_code)]
14//! - Ferguson, H.R.P., & Bailey, D.H. (1992). "A Polynomial Time, Numerically
15//!   Stable Integer Relation Algorithm"
16//! - Bailey, D.H., & Broadhurst, D. (2000). "Parallel Integer Relation Detection"
17
18use crate::thresholds::EXACT_MATCH_TOLERANCE;
19use std::f64::consts::PI;
20
21/// Maximum iterations for PSLQ algorithm
22const MAX_ITERATIONS: usize = 10000;
23
24/// Default precision for PSLQ (number of decimal digits)
25pub const DEFAULT_PSLQ_PRECISION: usize = 50;
26
27/// Result of PSLQ integer relation search
28#[derive(Debug, Clone)]
29pub struct IntegerRelation {
30    /// The integer coefficients found (not all zeros)
31    pub coefficients: Vec<i64>,
32    /// The basis vectors that were searched
33    pub basis_names: Vec<String>,
34    /// The residual error (should be near zero if a relation exists)
35    pub residual: f64,
36    /// Whether the relation is considered exact
37    pub is_exact: bool,
38}
39
40impl IntegerRelation {
41    /// Format the relation as a human-readable string
42    pub fn format(&self) -> String {
43        let terms: Vec<String> = self
44            .coefficients
45            .iter()
46            .zip(self.basis_names.iter())
47            .filter(|(c, _)| **c != 0)
48            .map(|(c, name)| {
49                if *c == 1 {
50                    name.clone()
51                } else if *c == -1 {
52                    format!("-{}", name)
53                } else {
54                    format!("{}*{}", c, name)
55                }
56            })
57            .collect();
58
59        if terms.is_empty() {
60            "0".to_string()
61        } else {
62            terms.join(" + ").replace("+ -", "- ")
63        }
64    }
65}
66
67/// Standard mathematical constants for PSLQ searches
68pub fn standard_constants() -> Vec<(String, f64)> {
69    vec![
70        ("1".to_string(), 1.0),
71        ("π".to_string(), PI),
72        ("π²".to_string(), PI * PI),
73        ("π³".to_string(), PI * PI * PI),
74        ("e".to_string(), std::f64::consts::E),
75        ("e²".to_string(), std::f64::consts::E * std::f64::consts::E),
76        ("e^π".to_string(), std::f64::consts::E.powf(PI)),
77        ("ln(2)".to_string(), (2.0f64).ln()),
78        ("ln(π)".to_string(), PI.ln()),
79        ("√2".to_string(), std::f64::consts::SQRT_2),
80        ("√π".to_string(), PI.sqrt()),
81        ("φ".to_string(), (1.0 + 5.0f64.sqrt()) / 2.0), // Golden ratio
82        ("γ".to_string(), 0.5772156649015329),          // Euler-Mascheroni
83        ("ζ(2)".to_string(), PI * PI / 6.0),            // Basel problem
84        ("ζ(3)".to_string(), 1.202056903159594),        // Apéry's constant
85        ("G".to_string(), 0.915965594177219),           // Catalan's constant
86    ]
87}
88
89/// Extended constants for thorough searches
90pub fn extended_constants() -> Vec<(String, f64)> {
91    let mut constants = standard_constants();
92    constants.extend(vec![
93        ("√3".to_string(), 3.0f64.sqrt()),
94        ("√5".to_string(), 5.0f64.sqrt()),
95        ("√7".to_string(), 7.0f64.sqrt()),
96        ("ln(3)".to_string(), (3.0f64).ln()),
97        ("ln(5)".to_string(), (5.0f64).ln()),
98        ("ln(7)".to_string(), (7.0f64).ln()),
99        ("π*√2".to_string(), PI * std::f64::consts::SQRT_2),
100        ("e+π".to_string(), std::f64::consts::E + PI),
101        ("e*π".to_string(), std::f64::consts::E * PI),
102        ("2^π".to_string(), 2.0f64.powf(PI)),
103        ("π^e".to_string(), PI.powf(std::f64::consts::E)),
104    ]);
105    constants
106}
107
108/// PSLQ algorithm configuration
109#[derive(Debug, Clone)]
110pub struct PslqConfig {
111    /// Maximum coefficient magnitude to search
112    pub max_coefficient: i64,
113    /// Maximum number of iterations
114    pub max_iterations: usize,
115    /// Tolerance for detecting zero relations
116    pub tolerance: f64,
117    /// Whether to use extended constant set
118    pub extended_constants: bool,
119}
120
121impl Default for PslqConfig {
122    fn default() -> Self {
123        Self {
124            max_coefficient: 1000,
125            max_iterations: MAX_ITERATIONS,
126            tolerance: EXACT_MATCH_TOLERANCE,
127            extended_constants: false,
128        }
129    }
130}
131
132/// Search for integer relations using PSLQ algorithm
133///
134/// Given a target value and a set of basis vectors (constants), find integer
135/// coefficients such that the linear combination is close to zero.
136///
137/// # Arguments
138///
139/// * `target` - The value to find relations for
140/// * `config` - PSLQ configuration options
141///
142/// # Returns
143///
144/// An optional integer relation if one is found
145pub fn find_integer_relation(target: f64, config: &PslqConfig) -> Option<IntegerRelation> {
146    // Get the basis constants
147    let constants = if config.extended_constants {
148        extended_constants()
149    } else {
150        standard_constants()
151    };
152
153    // Build the vector: [target, c1, c2, c3, ...]
154    // We want to find a relation a0*target + a1*c1 + ... = 0
155    let _n = constants.len() + 1;
156    let mut x: Vec<f64> = vec![target];
157    for (_, val) in &constants {
158        x.push(*val);
159    }
160
161    // Run PSLQ
162    let coefficients = pslq(&x, config)?;
163
164    // Check if the first coefficient (for target) is non-zero
165    if coefficients[0] == 0 {
166        return None;
167    }
168
169    // Calculate residual
170    let mut residual = 0.0;
171    for (i, c) in coefficients.iter().enumerate() {
172        residual += (*c as f64) * x[i];
173    }
174    residual = residual.abs();
175
176    // Check if residual is small enough
177    if residual > config.tolerance * target.abs().max(1.0) {
178        return None;
179    }
180
181    // Build basis names
182    let mut basis_names = vec!["x".to_string()];
183    for (name, _) in &constants {
184        basis_names.push(name.clone());
185    }
186
187    Some(IntegerRelation {
188        coefficients,
189        basis_names,
190        residual,
191        is_exact: residual < EXACT_MATCH_TOLERANCE,
192    })
193}
194
195/// Core PSLQ algorithm implementation
196///
197/// Finds integer relations among a vector of real numbers.
198/// Based on the algorithm from Ferguson & Bailey (1992).
199fn pslq(x: &[f64], config: &PslqConfig) -> Option<Vec<i64>> {
200    let n = x.len();
201    if n < 2 {
202        return None;
203    }
204
205    // Initialize the matrices
206    let _gamma = (4.0 / 3.0_f64).sqrt(); // Used in full PSLQ for reduction parameter
207
208    // Compute initial norms and scale the vector
209    let mut s: Vec<f64> = vec![0.0; n];
210    s[n - 1] = x[n - 1].abs();
211    for i in (0..n - 1).rev() {
212        s[i] = (s[i + 1].powi(2) + x[i].powi(2)).sqrt();
213    }
214
215    let scale = s[0];
216    if scale == 0.0 {
217        return None;
218    }
219
220    // Normalize
221    let mut y: Vec<f64> = x.iter().map(|xi| xi / scale).collect();
222    for i in 0..n {
223        s[i] /= scale;
224    }
225
226    // Initialize H matrix (upper triangular)
227    let mut h: Vec<Vec<f64>> = vec![vec![0.0; n]; n - 1];
228    for i in 0..n - 1 {
229        for j in 0..=i.min(n - 2) {
230            if i == j {
231                h[i][j] = s[j + 1] / s[j];
232            } else {
233                h[i][j] = -y[j] * y[i + 1] / (s[i] * s[i + 1]);
234            }
235        }
236    }
237
238    // Initialize A and B matrices (identity)
239    let mut a: Vec<Vec<i64>> = vec![vec![0; n]; n];
240    let mut b: Vec<Vec<i64>> = vec![vec![0; n]; n];
241    for i in 0..n {
242        a[i][i] = 1;
243        b[i][i] = 1;
244    }
245
246    // Main iteration loop
247    for _iteration in 0..config.max_iterations {
248        // Find the largest |h[i][i]|
249        let mut max_val = 0.0;
250        let mut max_idx = 0;
251        for i in 0..n - 1 {
252            let val = h[i][i].abs();
253            if val > max_val {
254                max_val = val;
255                max_idx = i;
256            }
257        }
258
259        // Swap rows max_idx and max_idx + 1
260        if max_idx < n - 1 {
261            y.swap(max_idx, max_idx + 1);
262            a.swap(max_idx, max_idx + 1);
263            b.swap(max_idx, max_idx + 1);
264            for j in 0..n - 1 {
265                let tmp = h[max_idx][j];
266                h[max_idx][j] = h[max_idx + 1][j];
267                h[max_idx + 1][j] = tmp;
268            }
269        }
270
271        // Reduction step
272        for i in (1..n).rev() {
273            if max_idx < n - 1 && h[max_idx][max_idx].abs() > 1e-50 {
274                let t = (h[i - 1][max_idx] / h[max_idx][max_idx]).round();
275                y[i - 1] -= t * y[max_idx];
276                for j in 0..n - 1 {
277                    h[i - 1][j] -= t * h[max_idx][j];
278                }
279                for j in 0..n {
280                    a[i - 1][j] -= (t as i64) * a[max_idx][j];
281                    b[j][i - 1] -= (t as i64) * b[j][max_idx];
282                }
283            }
284        }
285
286        // Check for small y[i] (found a relation)
287        for i in 0..n {
288            if y[i].abs() < config.tolerance {
289                // Found a relation - return coefficients from B matrix
290                let coeffs: Vec<i64> = (0..n).map(|j| b[j][i]).collect();
291
292                // Check coefficient bounds
293                if coeffs.iter().all(|&c| c.abs() <= config.max_coefficient) {
294                    return Some(coeffs);
295                }
296            }
297        }
298
299        // Termination check based on diagonal elements
300        let mut min_diag = f64::MAX;
301        for i in 0..n - 1 {
302            if h[i][i].abs() < min_diag {
303                min_diag = h[i][i].abs();
304            }
305        }
306
307        if min_diag < config.tolerance {
308            // Found a relation
309            let coeffs: Vec<i64> = (0..n).map(|j| b[j][0]).collect();
310            if coeffs.iter().all(|&c| c.abs() <= config.max_coefficient) {
311                return Some(coeffs);
312            }
313        }
314    }
315
316    None
317}
318
319/// Find rational approximation using continued fractions
320///
321/// This is simpler than PSLQ but only finds rational relations (a/b).
322pub fn find_rational_approximation(x: f64, max_denominator: i64) -> Option<(i64, i64)> {
323    let a0 = x.floor() as i64;
324    let mut remainder = x - a0 as f64;
325
326    if remainder.abs() < EXACT_MATCH_TOLERANCE {
327        return Some((a0, 1));
328    }
329
330    // Continued fraction expansion
331    let mut h_prev = 1i64;
332    let mut h_curr = a0;
333    let mut k_prev = 0i64;
334    let mut k_curr = 1i64;
335
336    for _ in 0..100 {
337        if remainder.abs() < EXACT_MATCH_TOLERANCE {
338            break;
339        }
340
341        let reciprocal = 1.0 / remainder;
342        let a = reciprocal.floor() as i64;
343        remainder = reciprocal - a as f64;
344
345        let h_next = a * h_curr + h_prev;
346        let k_next = a * k_curr + k_prev;
347
348        // Check denominator bound
349        if k_next > max_denominator {
350            break;
351        }
352
353        h_prev = h_curr;
354        h_curr = h_next;
355        k_prev = k_curr;
356        k_curr = k_next;
357
358        // Check if this is a good approximation
359        let approx = h_curr as f64 / k_curr as f64;
360        if (approx - x).abs() < EXACT_MATCH_TOLERANCE {
361            return Some((h_curr, k_curr));
362        }
363    }
364
365    // Return the best approximation found
366    if k_curr > 0 && k_curr <= max_denominator {
367        let approx = h_curr as f64 / k_curr as f64;
368        if (approx - x).abs() < x.abs() * 0.01 {
369            return Some((h_curr, k_curr));
370        }
371    }
372
373    None
374}
375
376/// Find minimal polynomial using LLL-based approach (simplified)
377///
378/// Given a value x, attempts to find a polynomial with integer coefficients
379/// that has x as a root. This is a simplified implementation.
380pub fn find_minimal_polynomial(x: f64, max_degree: usize, max_coeff: i64) -> Option<Vec<i64>> {
381    // Try polynomials of increasing degree
382    for degree in 1..=max_degree {
383        // Build a lattice basis for the polynomial coefficients
384        // This is a simplified approach - a full implementation would use LLL
385
386        // For small degrees, try to fit the polynomial directly
387        if let Some(coeffs) = try_polynomial_degree(x, degree, max_coeff) {
388            return Some(coeffs);
389        }
390    }
391    None
392}
393
394/// Try to find polynomial coefficients for a given degree
395fn try_polynomial_degree(x: f64, degree: usize, max_coeff: i64) -> Option<Vec<i64>> {
396    if degree == 0 {
397        return None;
398    }
399
400    // Compute powers of x
401    let mut powers = vec![1.0; degree + 1];
402    for i in 1..=degree {
403        powers[i] = powers[i - 1] * x;
404    }
405
406    // For degree 1: find a*x + b ≈ 0
407    // For degree 2: find a*x² + b*x + c ≈ 0
408    // etc.
409
410    // Use a simple search for small coefficients
411    let mut best_coeffs: Option<Vec<i64>> = None;
412    let mut best_error = f64::MAX;
413
414    // Search space - limit based on max_coeff
415    let search_range = (-(max_coeff / 10).max(1)..=(max_coeff / 10).max(1)).collect::<Vec<_>>();
416
417    // For very small degrees, exhaustive search is feasible
418    if degree <= 2 {
419        for coeffs in coefficients_product(&search_range, degree + 1) {
420            let mut value = 0.0;
421            for (i, c) in coeffs.iter().enumerate() {
422                value += (*c as f64) * powers[i];
423            }
424
425            let error = value.abs();
426            if error < best_error && error < EXACT_MATCH_TOLERANCE * 100.0 {
427                best_error = error;
428                best_coeffs = Some(coeffs);
429            }
430        }
431    }
432
433    best_coeffs
434}
435
436/// Generate all combinations of coefficients
437fn coefficients_product(ranges: &[i64], count: usize) -> Vec<Vec<i64>> {
438    if count == 0 {
439        return vec![vec![]];
440    }
441
442    let mut result = Vec::new();
443    let rest = coefficients_product(ranges, count - 1);
444    for r in rest {
445        for &val in ranges {
446            let mut combo = r.clone();
447            combo.push(val);
448            result.push(combo);
449        }
450    }
451    result
452}
453
454#[cfg(test)]
455mod tests {
456    use super::*;
457
458    #[test]
459    fn test_rational_approximation_pi() {
460        // π ≈ 355/113
461        let result = find_rational_approximation(PI, 1000);
462        assert!(result.is_some());
463        let (num, den) = result.unwrap();
464        assert_eq!(num, 355);
465        assert_eq!(den, 113);
466    }
467
468    #[test]
469    fn test_rational_approximation_sqrt2() {
470        // √2 ≈ 99/70 or 140/99
471        let result = find_rational_approximation(std::f64::consts::SQRT_2, 200);
472        assert!(result.is_some());
473        let (num, den) = result.unwrap();
474        let approx = num as f64 / den as f64;
475        assert!((approx - std::f64::consts::SQRT_2).abs() < 0.001);
476    }
477
478    #[test]
479    fn test_integer_relation_simple() {
480        // 2π - 6.283... ≈ 0
481        let config = PslqConfig::default();
482        let result = find_integer_relation(2.0 * PI, &config);
483        // Should find relation involving π
484        if let Some(rel) = result {
485            assert!(rel.residual < 0.01);
486        }
487    }
488
489    #[test]
490    fn test_minimal_polynomial_sqrt2() {
491        // √2 is a root of x² - 2 = 0
492        let result = find_minimal_polynomial(std::f64::consts::SQRT_2, 4, 100);
493        if let Some(coeffs) = result {
494            // Should be something like [-2, 0, 1] for x² - 2
495            let value: f64 = coeffs
496                .iter()
497                .enumerate()
498                .map(|(i, c)| *c as f64 * std::f64::consts::SQRT_2.powi(i as i32))
499                .sum();
500            assert!(value.abs() < 0.01);
501        }
502    }
503}