1#![allow(clippy::needless_range_loop)]
13#![allow(dead_code)]
14use crate::thresholds::EXACT_MATCH_TOLERANCE;
19use std::f64::consts::PI;
20
21const MAX_ITERATIONS: usize = 10000;
23
24pub const DEFAULT_PSLQ_PRECISION: usize = 50;
26
27#[derive(Debug, Clone)]
29pub struct IntegerRelation {
30 pub coefficients: Vec<i64>,
32 pub basis_names: Vec<String>,
34 pub residual: f64,
36 pub is_exact: bool,
38}
39
40impl IntegerRelation {
41 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
67pub 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), ("γ".to_string(), 0.5772156649015329), ("ζ(2)".to_string(), PI * PI / 6.0), ("ζ(3)".to_string(), 1.202056903159594), ("G".to_string(), 0.915965594177219), ]
87}
88
89pub 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#[derive(Debug, Clone)]
110pub struct PslqConfig {
111 pub max_coefficient: i64,
113 pub max_iterations: usize,
115 pub tolerance: f64,
117 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
132pub fn find_integer_relation(target: f64, config: &PslqConfig) -> Option<IntegerRelation> {
146 let constants = if config.extended_constants {
148 extended_constants()
149 } else {
150 standard_constants()
151 };
152
153 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 let coefficients =
162 find_two_term_relation(target, &constants, config).or_else(|| pslq(&x, config))?;
163
164 if coefficients[0] == 0 {
166 return None;
167 }
168
169 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 if residual > config.tolerance * target.abs().max(1.0) {
178 return None;
179 }
180
181 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
195fn find_two_term_relation(
196 target: f64,
197 constants: &[(String, f64)],
198 config: &PslqConfig,
199) -> Option<Vec<i64>> {
200 let residual_tolerance = config.tolerance * target.abs().max(1.0);
201 let relation_len = constants.len() + 1;
202 let mut best: Option<(Vec<i64>, i64, f64)> = None;
203
204 for (idx, (_, value)) in constants.iter().enumerate() {
205 let value = *value;
206 if !value.is_finite() {
207 continue;
208 }
209
210 let direct_residual = (target - value).abs();
211 if direct_residual <= residual_tolerance {
212 let mut coeffs = vec![0_i128; relation_len];
213 coeffs[0] = 1;
214 coeffs[idx + 1] = -1;
215 if let Some(normalized) = normalize_relation(coeffs, config.max_coefficient) {
216 return Some(normalized);
217 }
218 }
219
220 if value == 0.0 {
221 continue;
222 }
223
224 let Some((num, den)) = find_rational_approximation(target / value, config.max_coefficient)
225 else {
226 continue;
227 };
228 if den == 0 || num.abs() > config.max_coefficient || den.abs() > config.max_coefficient {
229 continue;
230 }
231
232 let residual = ((den as f64) * target - (num as f64) * value).abs();
233 if residual > residual_tolerance {
234 continue;
235 }
236
237 let mut coeffs = vec![0_i128; relation_len];
238 coeffs[0] = den as i128;
239 coeffs[idx + 1] = -(num as i128);
240 let Some(normalized) = normalize_relation(coeffs, config.max_coefficient) else {
241 continue;
242 };
243
244 let height = normalized
245 .iter()
246 .map(|coeff| coeff.abs())
247 .max()
248 .unwrap_or(config.max_coefficient);
249 match &best {
250 None => best = Some((normalized, height, residual)),
251 Some((_, best_height, best_residual)) => {
252 if height < *best_height
253 || (height == *best_height && residual + residual_tolerance < *best_residual)
254 {
255 best = Some((normalized, height, residual));
256 }
257 }
258 }
259 }
260
261 best.map(|(coeffs, _, _)| coeffs)
262}
263
264fn pslq(x: &[f64], config: &PslqConfig) -> Option<Vec<i64>> {
269 let n = x.len();
270 if n < 2 || x.iter().any(|value| !value.is_finite()) {
271 return None;
272 }
273
274 let gamma = (4.0 / 3.0_f64).sqrt();
278
279 let mut s: Vec<f64> = vec![0.0; n];
281 s[n - 1] = x[n - 1].abs();
282 for i in (0..n - 1).rev() {
283 s[i] = (s[i + 1].powi(2) + x[i].powi(2)).sqrt();
284 }
285
286 let scale = s[0];
287 if scale <= f64::EPSILON || !scale.is_finite() {
288 return None;
289 }
290
291 let mut y: Vec<f64> = x.iter().map(|xi| xi / scale).collect();
293 for value in &mut s {
294 *value /= scale;
295 }
296
297 let mut h: Vec<Vec<f64>> = vec![vec![0.0; n - 1]; n];
299 for i in 0..n {
300 for j in 0..n - 1 {
301 if i == j {
302 h[i][j] = s[j + 1] / s[j];
303 } else if i > j {
304 h[i][j] = -y[i] * y[j] / (s[j] * s[j + 1]);
305 } else {
306 h[i][j] = 0.0;
307 }
308 }
309 }
310
311 let mut a: Vec<Vec<i128>> = vec![vec![0; n]; n];
313 let mut b: Vec<Vec<i128>> = vec![vec![0; n]; n];
314 for i in 0..n {
315 a[i][i] = 1;
316 b[i][i] = 1;
317 }
318
319 reduce_h(&mut y, &mut h, &mut a, &mut b, 1, n - 2);
320
321 for _iteration in 0..config.max_iterations {
323 if let Some(coeffs) = detect_relation(x, &y, &b, config.max_coefficient, config.tolerance) {
324 return Some(coeffs);
325 }
326
327 let mut max_metric = 0.0;
329 let mut max_idx = 0;
330 for i in 0..n - 1 {
331 let metric = gamma.powi(i as i32) * h[i][i].abs();
332 if metric > max_metric {
333 max_metric = metric;
334 max_idx = i;
335 }
336 }
337
338 y.swap(max_idx, max_idx + 1);
340 a.swap(max_idx, max_idx + 1);
341 h.swap(max_idx, max_idx + 1);
342 for row in &mut b {
343 row.swap(max_idx, max_idx + 1);
344 }
345
346 remove_corner(&mut h, max_idx);
347
348 reduce_h(
350 &mut y,
351 &mut h,
352 &mut a,
353 &mut b,
354 max_idx + 1,
355 (max_idx + 1).min(n - 2),
356 );
357
358 if let Some(coeffs) = detect_relation(x, &y, &b, config.max_coefficient, config.tolerance) {
359 return Some(coeffs);
360 }
361
362 let max_diag = (0..n - 1).map(|i| h[i][i].abs()).fold(0.0_f64, f64::max);
363 if max_diag <= f64::EPSILON {
364 break;
365 }
366
367 let norm_lower_bound = 1.0 / max_diag;
370 let coefficient_norm_cap = (config.max_coefficient as f64) * (n as f64).sqrt();
371 if norm_lower_bound > coefficient_norm_cap {
372 break;
373 }
374
375 if max_abs_matrix_entry(&a) > (1_i128 << 52) {
378 break;
379 }
380 }
381
382 detect_relation(x, &y, &b, config.max_coefficient, config.tolerance)
383}
384
385fn reduce_h(
386 y: &mut [f64],
387 h: &mut [Vec<f64>],
388 a: &mut [Vec<i128>],
389 b: &mut [Vec<i128>],
390 row_start: usize,
391 max_active_col: usize,
392) {
393 if h.is_empty() || h[0].is_empty() || row_start >= h.len() {
394 return;
395 }
396
397 let max_col_count = h[0].len();
398 let active_col_count = (max_active_col + 1).min(max_col_count);
399
400 for i in row_start.max(1)..h.len() {
401 let upper = i.min(active_col_count);
402 for j in (0..upper).rev() {
403 let denom = h[j][j];
404 if denom.abs() <= f64::EPSILON {
405 continue;
406 }
407
408 let t = (h[i][j] / denom).round();
409 if t == 0.0 {
410 continue;
411 }
412
413 y[i] -= t * y[j];
414 for k in 0..=j {
415 h[i][k] -= t * h[j][k];
416 }
417
418 let t_int = t as i128;
419 for k in 0..a[i].len() {
420 a[i][k] -= t_int * a[j][k];
421 b[k][j] += t_int * b[k][i];
422 }
423 }
424 }
425}
426
427fn remove_corner(h: &mut [Vec<f64>], pivot_row: usize) {
428 if h.is_empty() || h[0].len() < 2 || pivot_row + 1 >= h[0].len() {
429 return;
430 }
431
432 let corner = h[pivot_row][pivot_row + 1];
433 if corner.abs() <= f64::EPSILON {
434 return;
435 }
436
437 let diagonal = h[pivot_row][pivot_row];
438 let norm = (diagonal * diagonal + corner * corner).sqrt();
439 if norm <= f64::EPSILON {
440 return;
441 }
442
443 let c = diagonal / norm;
444 let s = corner / norm;
445 for row in pivot_row..h.len() {
446 let left = h[row][pivot_row];
447 let right = h[row][pivot_row + 1];
448 h[row][pivot_row] = c * left + s * right;
449 h[row][pivot_row + 1] = -s * left + c * right;
450 }
451}
452
453fn detect_relation(
454 x: &[f64],
455 y: &[f64],
456 b: &[Vec<i128>],
457 max_coefficient: i64,
458 tolerance: f64,
459) -> Option<Vec<i64>> {
460 let mut candidate_order: Vec<usize> = (0..y.len()).collect();
461 candidate_order.sort_by(|&left, &right| y[left].abs().total_cmp(&y[right].abs()));
462
463 let residual_tolerance = tolerance * x.iter().map(|value| value.abs()).sum::<f64>().max(1.0);
464 let mut best: Option<(Vec<i64>, f64, f64)> = None;
465
466 for idx in candidate_order {
467 let coeffs: Vec<i128> = (0..y.len()).map(|row| b[row][idx]).collect();
468 let Some(normalized) = normalize_relation(coeffs, max_coefficient) else {
469 continue;
470 };
471
472 let residual = x
473 .iter()
474 .zip(normalized.iter())
475 .map(|(value, coeff)| value * (*coeff as f64))
476 .sum::<f64>()
477 .abs();
478 if residual > residual_tolerance {
479 continue;
480 }
481
482 let y_magnitude = y[idx].abs();
483 match &best {
484 None => best = Some((normalized, residual, y_magnitude)),
485 Some((_, best_residual, best_y)) => {
486 let clearly_better = residual + residual_tolerance < *best_residual;
487 let same_residual = (residual - *best_residual).abs() <= residual_tolerance;
488 if clearly_better || (same_residual && y_magnitude < *best_y) {
489 best = Some((normalized, residual, y_magnitude));
490 }
491 }
492 }
493 }
494
495 best.map(|(coeffs, _, _)| coeffs)
496}
497
498fn normalize_relation(coeffs: Vec<i128>, max_coefficient: i64) -> Option<Vec<i64>> {
499 if coeffs.iter().all(|&coeff| coeff == 0) {
500 return None;
501 }
502
503 let mut gcd = 0_i128;
504 for &coeff in &coeffs {
505 gcd = gcd_i128(gcd, coeff.abs());
506 }
507
508 let mut normalized = if gcd > 1 {
509 coeffs
510 .into_iter()
511 .map(|coeff| coeff / gcd)
512 .collect::<Vec<_>>()
513 } else {
514 coeffs
515 };
516
517 if let Some(&first_non_zero) = normalized.iter().find(|&&coeff| coeff != 0) {
518 if first_non_zero < 0 {
519 for coeff in &mut normalized {
520 *coeff = -*coeff;
521 }
522 }
523 }
524
525 let cap = i128::from(max_coefficient);
526 if normalized.iter().any(|&coeff| coeff.abs() > cap) {
527 return None;
528 }
529
530 normalized
531 .into_iter()
532 .map(|coeff| i64::try_from(coeff).ok())
533 .collect()
534}
535
536fn gcd_i128(mut left: i128, mut right: i128) -> i128 {
537 if left == 0 {
538 return right;
539 }
540 if right == 0 {
541 return left;
542 }
543
544 while right != 0 {
545 let remainder = left % right;
546 left = right;
547 right = remainder;
548 }
549 left.abs()
550}
551
552fn max_abs_matrix_entry(matrix: &[Vec<i128>]) -> i128 {
553 matrix
554 .iter()
555 .flat_map(|row| row.iter())
556 .map(|value| value.abs())
557 .max()
558 .unwrap_or(0)
559}
560
561pub fn find_rational_approximation(x: f64, max_denominator: i64) -> Option<(i64, i64)> {
565 let a0 = x.floor() as i64;
566 let mut remainder = x - a0 as f64;
567
568 if remainder.abs() < EXACT_MATCH_TOLERANCE {
569 return Some((a0, 1));
570 }
571
572 let mut h_prev = 1i64;
574 let mut h_curr = a0;
575 let mut k_prev = 0i64;
576 let mut k_curr = 1i64;
577
578 for _ in 0..100 {
579 if remainder.abs() < EXACT_MATCH_TOLERANCE {
580 break;
581 }
582
583 let reciprocal = 1.0 / remainder;
584 let a = reciprocal.floor() as i64;
585 remainder = reciprocal - a as f64;
586
587 let h_next = a * h_curr + h_prev;
588 let k_next = a * k_curr + k_prev;
589
590 if k_next > max_denominator {
592 break;
593 }
594
595 h_prev = h_curr;
596 h_curr = h_next;
597 k_prev = k_curr;
598 k_curr = k_next;
599
600 let approx = h_curr as f64 / k_curr as f64;
602 if (approx - x).abs() < EXACT_MATCH_TOLERANCE {
603 return Some((h_curr, k_curr));
604 }
605 }
606
607 if k_curr > 0 && k_curr <= max_denominator {
609 let approx = h_curr as f64 / k_curr as f64;
610 if (approx - x).abs() < x.abs() * 0.01 {
611 return Some((h_curr, k_curr));
612 }
613 }
614
615 None
616}
617
618pub fn find_minimal_polynomial(x: f64, max_degree: usize, max_coeff: i64) -> Option<Vec<i64>> {
623 for degree in 1..=max_degree {
625 if let Some(coeffs) = try_polynomial_degree(x, degree, max_coeff) {
630 return Some(coeffs);
631 }
632 }
633 None
634}
635
636fn try_polynomial_degree(x: f64, degree: usize, max_coeff: i64) -> Option<Vec<i64>> {
638 if degree == 0 {
639 return None;
640 }
641
642 let mut powers = vec![1.0; degree + 1];
644 for i in 1..=degree {
645 powers[i] = powers[i - 1] * x;
646 }
647
648 let mut best_coeffs: Option<Vec<i64>> = None;
654 let mut best_error = f64::MAX;
655
656 let search_range = (-(max_coeff / 10).max(1)..=(max_coeff / 10).max(1)).collect::<Vec<_>>();
658
659 if degree <= 2 {
661 for coeffs in coefficients_product(&search_range, degree + 1) {
662 let mut value = 0.0;
663 for (i, c) in coeffs.iter().enumerate() {
664 value += (*c as f64) * powers[i];
665 }
666
667 let error = value.abs();
668 if error < best_error && error < EXACT_MATCH_TOLERANCE * 100.0 {
669 best_error = error;
670 best_coeffs = Some(coeffs);
671 }
672 }
673 }
674
675 best_coeffs
676}
677
678fn coefficients_product(ranges: &[i64], count: usize) -> Vec<Vec<i64>> {
680 if count == 0 {
681 return vec![vec![]];
682 }
683
684 let mut result = Vec::new();
685 let rest = coefficients_product(ranges, count - 1);
686 for r in rest {
687 for &val in ranges {
688 let mut combo = r.clone();
689 combo.push(val);
690 result.push(combo);
691 }
692 }
693 result
694}
695
696#[cfg(test)]
697mod tests {
698 use super::*;
699
700 #[test]
701 fn test_rational_approximation_pi() {
702 let result = find_rational_approximation(PI, 1000);
704 assert!(result.is_some());
705 let (num, den) = result.unwrap();
706 assert_eq!(num, 355);
707 assert_eq!(den, 113);
708 }
709
710 #[test]
711 fn test_rational_approximation_sqrt2() {
712 let result = find_rational_approximation(std::f64::consts::SQRT_2, 200);
714 assert!(result.is_some());
715 let (num, den) = result.unwrap();
716 let approx = num as f64 / den as f64;
717 assert!((approx - std::f64::consts::SQRT_2).abs() < 0.001);
718 }
719
720 #[test]
721 fn test_integer_relation_simple() {
722 let config = PslqConfig::default();
723 let rel = find_integer_relation(2.0 * PI, &config).expect("2*pi should be found");
724 assert_eq!(rel.format(), "x - 2*π");
725 assert!(rel.residual < EXACT_MATCH_TOLERANCE);
726 }
727
728 #[test]
729 fn test_pslq_duplicate_relation() {
730 let coeffs = pslq(&[PI, PI], &PslqConfig::default()).expect("duplicate relation");
731 assert_eq!(coeffs, vec![1, -1]);
732 }
733
734 #[test]
735 fn test_pslq_scalar_multiple_relation() {
736 let coeffs =
737 pslq(&[2.0 * PI, PI], &PslqConfig::default()).expect("scalar multiple relation");
738 assert_eq!(coeffs, vec![1, -2]);
739 }
740
741 #[test]
742 fn test_integer_relation_direct_basis_hits() {
743 let config = PslqConfig::default();
744
745 let pi = find_integer_relation(PI, &config).expect("pi should be found");
746 assert_eq!(pi.format(), "x - π");
747
748 let phi = find_integer_relation((1.0 + 5.0_f64.sqrt()) / 2.0, &config)
749 .expect("phi should be found");
750 assert_eq!(phi.format(), "x - φ");
751
752 let sqrt_pi = find_integer_relation(PI.sqrt(), &config).expect("sqrt(pi) should be found");
753 assert_eq!(sqrt_pi.format(), "x - √π");
754
755 let zeta2 = find_integer_relation(PI * PI / 6.0, &config).expect("zeta(2) should be found");
756 assert_eq!(zeta2.format(), "x - ζ(2)");
757 }
758
759 #[test]
760 fn test_minimal_polynomial_sqrt2() {
761 let result = find_minimal_polynomial(std::f64::consts::SQRT_2, 4, 100);
763 if let Some(coeffs) = result {
764 let value: f64 = coeffs
766 .iter()
767 .enumerate()
768 .map(|(i, c)| *c as f64 * std::f64::consts::SQRT_2.powi(i as i32))
769 .sum();
770 assert!(value.abs() < 0.01);
771 }
772 }
773
774 #[test]
775 fn test_pslq_last_diagonal_no_panic() {
776 let config = PslqConfig {
777 max_iterations: 1,
778 ..PslqConfig::default()
779 };
780
781 let _ = pslq(&[100.0, 1.0, 1.0], &config);
782 }
783
784 #[test]
785 fn test_reduce_h_keeps_y_in_sync_with_a() {
786 let x: [f64; 3] = [10.0, 1.0, 1.0];
787 let n = x.len();
788
789 let mut s = vec![0.0; n];
790 s[n - 1] = x[n - 1].abs();
791 for i in (0..n - 1).rev() {
792 s[i] = (s[i + 1].powi(2) + x[i].powi(2)).sqrt();
793 }
794 let scale = s[0];
795
796 let mut y: Vec<f64> = x.iter().map(|value| value / scale).collect();
797 for value in &mut s {
798 *value /= scale;
799 }
800
801 let mut h = vec![vec![0.0; n - 1]; n];
802 for i in 0..n {
803 for j in 0..n - 1 {
804 if i == j {
805 h[i][j] = s[j + 1] / s[j];
806 } else if i > j {
807 h[i][j] = -y[i] * y[j] / (s[j] * s[j + 1]);
808 }
809 }
810 }
811
812 let mut a = vec![vec![0_i128; n]; n];
813 let mut b = vec![vec![0_i128; n]; n];
814 for i in 0..n {
815 a[i][i] = 1;
816 b[i][i] = 1;
817 }
818
819 reduce_h(&mut y, &mut h, &mut a, &mut b, 1, n - 2);
820 assert!(
821 max_abs_matrix_entry(&a) > 1,
822 "test vector should trigger a non-trivial reduction"
823 );
824
825 for i in 0..n {
826 let expected = a[i]
827 .iter()
828 .zip(x.iter())
829 .map(|(coeff, value)| (*coeff as f64) * *value / scale)
830 .sum::<f64>();
831 assert!(
832 (y[i] - expected).abs() < 1e-12,
833 "row {i} drifted: y={}, expected={expected}",
834 y[i]
835 );
836 }
837 }
838}