alkahest_cas/numeric/
pslq.rs1use crate::errors::AlkahestError;
7use crate::lattice::{lattice_reduce_rows, LatticeError};
8use rug::ops::PowAssign;
9use rug::{Assign, Float, Integer};
10use std::cmp::Ordering;
11use std::fmt;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum PslqError {
16 TooFewCoordinates,
17 AllZeroMagnitudes,
18 PrecisionTooThin { bits: u32 },
19 Lattice(LatticeError),
20}
21
22impl fmt::Display for PslqError {
23 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24 match self {
25 PslqError::TooFewCoordinates => write!(
26 f,
27 "guess_integer_relation needs at least two floating scalars"
28 ),
29 PslqError::AllZeroMagnitudes => write!(
30 f,
31 "scaled magnitudes vanished (check precision or literals)"
32 ),
33 PslqError::PrecisionTooThin { bits } => {
34 write!(f, "precision_bits ({bits}); require ≥64 MPFR bits")
35 }
36 PslqError::Lattice(e) => write!(f, "{e}"),
37 }
38 }
39}
40
41impl std::error::Error for PslqError {
42 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
43 match self {
44 PslqError::Lattice(e) => Some(e),
45 _ => None,
46 }
47 }
48}
49
50impl AlkahestError for PslqError {
51 fn code(&self) -> &'static str {
52 match self {
53 PslqError::TooFewCoordinates => "E-PSLQ-001",
54 PslqError::AllZeroMagnitudes => "E-PSLQ-002",
55 PslqError::PrecisionTooThin { .. } => "E-PSLQ-003",
56 PslqError::Lattice(e) => e.code(),
57 }
58 }
59
60 fn remediation(&self) -> Option<&'static str> {
61 match self {
62 PslqError::TooFewCoordinates => Some("pass [x₀,…,x_{n−1}] with n ≥ 2"),
63 PslqError::AllZeroMagnitudes => {
64 Some("use higher-precision inputs (strings or MPFR literals)")
65 }
66 PslqError::PrecisionTooThin { .. } => {
67 Some("raise precision_bits — ≈664 bits ≈ 200 decimal digits")
68 }
69 PslqError::Lattice(e) => e.remediation(),
70 }
71 }
72}
73
74fn lin_residual(bits: u32, coeffs: &[Integer], xs: &[Float]) -> Float {
75 let mut acc = Float::with_val(bits, 0);
76 for (c, xv) in coeffs.iter().zip(xs.iter()) {
77 let mut term = Float::with_val(bits, c);
78 term *= Float::with_val(bits, xv);
79 acc += term;
80 }
81 acc.abs_mut();
82 acc
83}
84
85pub fn guess_integer_relation(
89 xs: &[Float],
90 precision_bits: u32,
91 max_abs_coeff: Option<u128>,
92) -> Result<Option<Vec<Integer>>, PslqError> {
93 let n = xs.len();
94 if n < 2 {
95 return Err(PslqError::TooFewCoordinates);
96 }
97 if precision_bits < 64 {
98 return Err(PslqError::PrecisionTooThin {
99 bits: precision_bits,
100 });
101 }
102 let bits = precision_bits.min(16_384);
103
104 let mut normed: Vec<Float> = xs.iter().map(|xv| Float::with_val(bits, xv)).collect();
105 let mut ymax = Float::with_val(bits, 0);
106 for v in &normed {
107 let mut cp = Float::with_val(bits, v);
108 cp.abs_mut();
109 if cp.partial_cmp(&ymax) == Some(Ordering::Greater) {
110 ymax.assign(&cp);
111 }
112 }
113 let zero = Float::with_val(bits, 0);
114 if ymax.partial_cmp(&zero) == Some(Ordering::Equal) {
115 return Err(PslqError::AllZeroMagnitudes);
116 }
117
118 for v in &mut normed {
119 let mut quot = Float::with_val(bits, &*v);
120 quot /= &ymax;
121 v.assign(");
122 }
123
124 let shift_amt = (bits * 3 / 4).min(1536);
125 let mut scale = Integer::from(1u32);
126 scale <<= shift_amt;
127
128 let mut augmented: Vec<Vec<Integer>> = Vec::with_capacity(n);
129 for i in 0..n {
130 let mut row = vec![Integer::from(0); n + 1];
131 row[i] = Integer::from(1);
132 let mut xf = Float::with_val(bits, &normed[i]);
133 xf *= Float::with_val(bits, &scale);
134 let tail = xf.to_integer().ok_or(PslqError::AllZeroMagnitudes)?;
135 row[n].assign(&tail);
136 augmented.push(row);
137 }
138
139 let reduced = lattice_reduce_rows(&augmented).map_err(PslqError::Lattice)?;
140
141 let mut tol = Float::with_val(bits, 2);
142 let exp_lim = ((-((bits as f64) * 0.75).floor()) as i32).min(-1);
143 tol.pow_assign(exp_lim);
144 tol *= Float::with_val(bits, (n.max(1)) as i32);
145
146 let mut best: Option<(Vec<Integer>, Float)> = None;
147 for row in &reduced {
148 let coeffs: Vec<Integer> = row.iter().take(n).cloned().collect();
149 if coeffs.iter().all(Integer::is_zero) {
150 continue;
151 }
152 if let Some(limit) = max_abs_coeff {
153 let lim = Integer::from(limit);
154 let mut ok = true;
155 for z in &coeffs {
156 let mut a = z.clone();
157 a.abs_mut();
158 if a.cmp(&lim) == Ordering::Greater {
159 ok = false;
160 break;
161 }
162 }
163 if !ok {
164 continue;
165 }
166 }
167 let resid = lin_residual(bits, &coeffs, &normed);
168 let take = match &best {
169 None => true,
170 Some((_, r0)) => resid.partial_cmp(r0) == Some(Ordering::Less),
171 };
172 if take {
173 best = Some((coeffs, resid));
174 }
175 }
176
177 Ok(best.and_then(|(v, r)| {
178 if r.partial_cmp(&tol) != Some(Ordering::Greater) {
179 Some(v)
180 } else {
181 None
182 }
183 }))
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn relation_on_1_2_3() {
192 let bits = 256u32;
193 let xs = vec![
194 Float::with_val(bits, 1),
195 Float::with_val(bits, 2),
196 Float::with_val(bits, 3),
197 ];
198 let rel = guess_integer_relation(&xs, bits, Some(10_000))
199 .unwrap()
200 .unwrap();
201 let r = lin_residual(bits, &rel, &xs);
202 let mut tol = Float::with_val(bits, 2);
203 tol.pow_assign(-((bits as f64 * 0.75).floor() as i32));
204 tol *= Float::with_val(bits, 3);
205 assert!(
206 r.partial_cmp(&tol) != Some(Ordering::Greater),
207 "residual {r:?} tol {tol:?}"
208 );
209 }
210}