1use std::fmt::Display;
8
9use num_bigint::{BigInt, Sign};
10use num_rational::BigRational;
11use num_traits::{ToPrimitive, Zero};
12
13use ocas_domain::EuclideanDomain;
14
15use crate::dense::DenseUnivariatePolynomial;
16
17#[derive(Debug, Clone, PartialEq)]
19pub struct RootInterval {
20 pub low: f64,
22 pub high: f64,
24}
25
26impl<D: EuclideanDomain> DenseUnivariatePolynomial<D>
27where
28 D::Element: Display,
29{
30 pub fn sturm_sequence(&self) -> Vec<Self> {
35 let mut seq = Vec::new();
36 if self.is_zero() {
37 return seq;
38 }
39
40 seq.push(self.clone());
41 let deriv = self.derivative();
42 if deriv.is_zero() {
43 return seq;
44 }
45 seq.push(deriv);
46
47 loop {
48 let a = &seq[seq.len() - 2];
49 let b = &seq[seq.len() - 1];
50 if b.is_zero() {
51 break;
52 }
53 let rem = match a.pseudo_remainder(b) {
55 Some(r) => r,
56 None => break,
57 };
58 if rem.is_zero() {
59 break;
60 }
61 seq.push(rem.neg());
63 }
64
65 seq
66 }
67
68 pub fn eval_f64(&self, x: f64) -> f64 {
73 let mut result = 0.0;
74 for coeff in self.coeffs().iter().rev() {
75 result = result * x + coeff_value(coeff);
76 }
77 result
78 }
79
80 pub fn count_real_roots(&self) -> usize {
84 let seq = self.sturm_sequence();
85 if seq.len() < 2 {
86 return 0;
87 }
88 let neg_inf = count_sign_changes_at_infinity(&seq, true);
89 let pos_inf = count_sign_changes_at_infinity(&seq, false);
90 neg_inf.saturating_sub(pos_inf)
91 }
92
93 pub fn isolate_real_roots(&self) -> Vec<RootInterval> {
103 let seq = self.sturm_sequence();
104 if seq.len() < 2 {
105 return vec![];
106 }
107
108 let total_roots = self.count_real_roots();
109 if total_roots == 0 {
110 return vec![];
111 }
112
113 let m = root_bound(self);
115 if let Some(exact) = ExactSturm::prepare(&seq) {
116 return self.isolate_exact(&exact, m, total_roots);
117 }
118 self.isolate_f64(&seq, m, total_roots)
119 }
120
121 fn isolate_f64(
124 &self,
125 seq: &[DenseUnivariatePolynomial<D>],
126 m: f64,
127 total_roots: usize,
128 ) -> Vec<RootInterval> {
129 let mut intervals = Vec::new();
130 let mut stack = vec![(-m, m)];
131
132 while let Some((lo, hi)) = stack.pop() {
133 if intervals.len() >= total_roots {
134 break;
135 }
136
137 let lo_signs = count_sign_changes(seq, lo);
138 let hi_signs = count_sign_changes(seq, hi);
139 let count = lo_signs.saturating_sub(hi_signs);
140
141 if count == 0 {
142 continue;
143 }
144 if count == 1 && (hi - lo) < 1e-10 {
145 intervals.push(RootInterval { low: lo, high: hi });
146 continue;
147 }
148 if hi - lo < 1e-12 {
149 if count == 1 {
150 intervals.push(RootInterval { low: lo, high: hi });
151 }
152 continue;
153 }
154
155 let mid = (lo + hi) / 2.0;
156 stack.push((lo, mid));
157 stack.push((mid, hi));
158 }
159
160 intervals
161 }
162
163 fn isolate_exact(&self, seq: &ExactSturm, m: f64, total_roots: usize) -> Vec<RootInterval> {
166 let bound = BigInt::from(m.ceil() as i64);
167 let mut intervals = Vec::new();
168 let mut stack = vec![(-&bound, bound, 0u32)];
170
171 while let Some((lo_m, hi_m, k)) = stack.pop() {
172 if intervals.len() >= total_roots {
173 break;
174 }
175
176 let lo_signs = seq.count_at(&lo_m, k);
177 let hi_signs = seq.count_at(&hi_m, k);
178 let count = lo_signs.saturating_sub(hi_signs);
179
180 if count == 0 {
181 continue;
182 }
183 let width = (&hi_m - &lo_m).to_f64().unwrap_or(f64::INFINITY) / 2.0f64.powi(k as i32);
186 let low_f = dyadic_f64(&lo_m, k);
187 let high_f = dyadic_f64(&hi_m, k);
188 if count == 1 && width < 1e-10 {
189 intervals.push(RootInterval {
190 low: low_f,
191 high: high_f,
192 });
193 continue;
194 }
195 if width < 1e-12 {
196 if count == 1 {
197 intervals.push(RootInterval {
198 low: low_f,
199 high: high_f,
200 });
201 }
202 continue;
203 }
204
205 let lo2 = &lo_m << 1usize;
207 let hi2 = &hi_m << 1usize;
208 let mid = &lo_m + &hi_m;
209 stack.push((lo2, mid.clone(), k + 1));
210 stack.push((mid, hi2, k + 1));
211 }
212
213 intervals
214 }
215
216 pub fn refine_root(&self, interval: &RootInterval, tol: f64) -> RootInterval {
218 let mut lo = interval.low;
219 let mut hi = interval.high;
220 let f_lo = self.eval_f64(lo);
221
222 if f_lo.abs() < 1e-15 {
223 return RootInterval { low: lo, high: lo };
224 }
225
226 while hi - lo > tol {
227 let mid = (lo + hi) / 2.0;
228 let f_mid = self.eval_f64(mid);
229 if f_mid.abs() < 1e-15 {
230 return RootInterval {
231 low: mid,
232 high: mid,
233 };
234 }
235 if f_lo * f_mid < 0.0 {
236 hi = mid;
237 } else {
238 lo = mid;
239 }
240 }
241
242 RootInterval { low: lo, high: hi }
243 }
244}
245
246struct ExactSturm {
249 polys: Vec<Vec<BigInt>>,
250}
251
252impl ExactSturm {
253 fn prepare<D: EuclideanDomain>(seq: &[DenseUnivariatePolynomial<D>]) -> Option<Self>
256 where
257 D::Element: Display,
258 {
259 let mut polys = Vec::with_capacity(seq.len());
260 for p in seq {
261 let mut coeffs = Vec::with_capacity(p.coeffs().len());
262 let mut lcm = BigInt::from(1);
263 for c in p.coeffs() {
264 let r = coeff_to_bigrational(c)?;
265 lcm = bigint_lcm(&lcm, r.denom());
266 coeffs.push(r);
267 }
268 let scale = BigRational::from_integer(lcm);
269 polys.push(coeffs.iter().map(|c| (c * &scale).to_integer()).collect());
270 }
271 Some(Self { polys })
272 }
273
274 fn count_at(&self, m: &BigInt, k: u32) -> usize {
278 let mut count = 0;
279 let mut prev: Option<bool> = None;
280 for icoeffs in &self.polys {
281 let sign = eval_sign_dyadic(icoeffs, m, k);
282 if sign == 0 {
283 continue;
284 }
285 let positive = sign > 0;
286 if let Some(p) = prev
287 && p != positive
288 {
289 count += 1;
290 }
291 prev = Some(positive);
292 }
293 count
294 }
295}
296
297fn eval_sign_dyadic(coeffs: &[BigInt], m: &BigInt, k: u32) -> i8 {
299 let n = coeffs.len().saturating_sub(1);
300 let mut s = BigInt::zero();
301 for (i, a) in coeffs.iter().enumerate() {
302 if a.is_zero() {
303 continue;
304 }
305 let mut t = a * m.pow(i as u32);
306 t <<= k as usize * (n - i);
307 s += t;
308 }
309 match s.sign() {
310 Sign::Plus => 1,
311 Sign::Minus => -1,
312 Sign::NoSign => 0,
313 }
314}
315
316fn dyadic_f64(m: &BigInt, k: u32) -> f64 {
318 m.to_f64().unwrap_or(f64::NAN) / 2.0f64.powi(k as i32)
319}
320
321fn coeff_to_bigrational(elem: &(impl Display + ?Sized)) -> Option<BigRational> {
324 elem.to_string().trim().parse::<BigRational>().ok()
325}
326
327fn bigint_lcm(a: &BigInt, b: &BigInt) -> BigInt {
329 let mut x = a.clone();
330 let mut y = b.clone();
331 while !y.is_zero() {
332 let r = x % &y;
333 x = y;
334 y = r;
335 }
336 if x.is_zero() {
337 return BigInt::from(1);
338 }
339 (a / &x) * b
340}
341
342fn count_sign_changes_at_infinity<D: EuclideanDomain>(
344 seq: &[DenseUnivariatePolynomial<D>],
345 at_neg_inf: bool,
346) -> usize
347where
348 D::Element: Display,
349{
350 let vals: Vec<f64> = seq
351 .iter()
352 .map(|p| {
353 if p.is_zero() {
354 return 0.0;
355 }
356 let deg = p.degree().unwrap_or(0);
357 let lc = coeff_value(p.leading_coeff().unwrap());
358 if at_neg_inf {
361 if deg % 2 == 0 { lc } else { -lc }
362 } else {
363 lc
364 }
365 })
366 .collect();
367 count_sign_changes_in_vals(&vals)
368}
369
370fn count_sign_changes<D: EuclideanDomain>(seq: &[DenseUnivariatePolynomial<D>], x: f64) -> usize
372where
373 D::Element: Display,
374{
375 let vals: Vec<f64> = seq.iter().map(|p| p.eval_f64(x)).collect();
376 count_sign_changes_in_vals(&vals)
377}
378
379fn count_sign_changes_in_vals(vals: &[f64]) -> usize {
380 let mut count = 0;
381 let mut prev_sign: Option<bool> = None;
382 for &v in vals {
383 if v == 0.0 {
384 continue;
385 }
386 let sign = v > 0.0;
387 if let Some(p) = prev_sign
388 && p != sign
389 {
390 count += 1;
391 }
392 prev_sign = Some(sign);
393 }
394 count
395}
396
397fn root_bound<D: EuclideanDomain>(p: &DenseUnivariatePolynomial<D>) -> f64
399where
400 D::Element: Display,
401{
402 if p.is_zero() || p.degree().is_none() {
403 return 1.0;
404 }
405 let coeffs = p.coeffs();
406 let lc = coeff_value(coeffs.last().unwrap()).abs();
407 let mut max_abs = 0.0f64;
408 for c in &coeffs[..coeffs.len() - 1] {
409 let v = coeff_value(c).abs();
410 if v > max_abs {
411 max_abs = v;
412 }
413 }
414 1.0 + max_abs / lc.max(1e-10)
415}
416
417fn coeff_value(elem: &(impl Display + ?Sized)) -> f64 {
419 let s = elem.to_string();
420 let trimmed = s.trim();
421 if let Ok(v) = trimmed.parse::<f64>() {
423 return v;
424 }
425 if let Ok(v) = trimmed.parse::<i64>() {
427 return v as f64;
428 }
429 if let Some((num_str, den_str)) = trimmed.split_once('/')
431 && let (Ok(n), Ok(d)) = (num_str.trim().parse::<f64>(), den_str.trim().parse::<f64>())
432 && d != 0.0
433 {
434 return n / d;
435 }
436 0.0
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442 use ocas_domain::{Integer, IntegerDomain};
443
444 fn i(n: i64) -> Integer {
445 Integer::from(n)
446 }
447
448 #[test]
449 fn count_roots_x2_minus_1() {
450 let d = IntegerDomain;
451 let p = DenseUnivariatePolynomial::from_coeffs(d, vec![i(-1), i(0), i(1)]);
452 assert_eq!(p.count_real_roots(), 2);
453 }
454
455 #[test]
456 fn count_roots_x2_plus_1() {
457 let d = IntegerDomain;
458 let p = DenseUnivariatePolynomial::from_coeffs(d, vec![i(1), i(0), i(1)]);
459 assert_eq!(p.count_real_roots(), 0);
460 }
461
462 #[test]
463 fn count_roots_perfect_square() {
464 let d = IntegerDomain;
465 let p = DenseUnivariatePolynomial::from_coeffs(d, vec![i(1), i(2), i(1)]);
467 assert_eq!(p.count_real_roots(), 1);
468 }
469
470 #[test]
471 fn isolate_roots_x2_minus_2() {
472 let d = IntegerDomain;
473 let p = DenseUnivariatePolynomial::from_coeffs(d, vec![i(-2), i(0), i(1)]);
474 let intervals = p.isolate_real_roots();
475 assert_eq!(intervals.len(), 2);
476 let refined = p.refine_root(&intervals[1], 1e-6);
478 let approx = (refined.low + refined.high) / 2.0;
479 assert!((approx.abs() - std::f64::consts::SQRT_2).abs() < 0.01);
480 }
481
482 #[test]
483 fn sturm_sequence_length() {
484 let d = IntegerDomain;
485 let p = DenseUnivariatePolynomial::from_coeffs(d, vec![i(-1), i(0), i(1)]);
486 let seq = p.sturm_sequence();
487 assert!(seq.len() >= 2);
488 }
489}