1use std::fmt::Display;
8
9use ocas_domain::EuclideanDomain;
10
11use crate::dense::DenseUnivariatePolynomial;
12
13#[derive(Debug, Clone, PartialEq)]
15pub struct RootInterval {
16 pub low: f64,
18 pub high: f64,
20}
21
22impl<D: EuclideanDomain> DenseUnivariatePolynomial<D>
23where
24 D::Element: Display,
25{
26 pub fn sturm_sequence(&self) -> Vec<Self> {
31 let mut seq = Vec::new();
32 if self.is_zero() {
33 return seq;
34 }
35
36 seq.push(self.clone());
37 let deriv = self.derivative();
38 if deriv.is_zero() {
39 return seq;
40 }
41 seq.push(deriv);
42
43 loop {
44 let a = &seq[seq.len() - 2];
45 let b = &seq[seq.len() - 1];
46 if b.is_zero() {
47 break;
48 }
49 let rem = match a.pseudo_remainder(b) {
51 Some(r) => r,
52 None => break,
53 };
54 if rem.is_zero() {
55 break;
56 }
57 seq.push(rem.neg());
59 }
60
61 seq
62 }
63
64 pub fn eval_f64(&self, x: f64) -> f64 {
69 let mut result = 0.0;
70 for coeff in self.coeffs().iter().rev() {
71 result = result * x + coeff_value(coeff);
72 }
73 result
74 }
75
76 pub fn count_real_roots(&self) -> usize {
80 let seq = self.sturm_sequence();
81 if seq.len() < 2 {
82 return 0;
83 }
84 let neg_inf = count_sign_changes_at_infinity(&seq, true);
85 let pos_inf = count_sign_changes_at_infinity(&seq, false);
86 neg_inf.saturating_sub(pos_inf)
87 }
88
89 pub fn isolate_real_roots(&self) -> Vec<RootInterval> {
94 let seq = self.sturm_sequence();
95 if seq.len() < 2 {
96 return vec![];
97 }
98
99 let total_roots = self.count_real_roots();
100 if total_roots == 0 {
101 return vec![];
102 }
103
104 let m = root_bound(self);
106 let mut intervals = Vec::new();
107 let mut stack = vec![(-m, m)];
108
109 while let Some((lo, hi)) = stack.pop() {
110 if intervals.len() >= total_roots {
111 break;
112 }
113
114 let lo_signs = count_sign_changes(&seq, lo);
115 let hi_signs = count_sign_changes(&seq, hi);
116 let count = lo_signs.saturating_sub(hi_signs);
117
118 if count == 0 {
119 continue;
120 }
121 if count == 1 && (hi - lo) < 1e-10 {
122 intervals.push(RootInterval { low: lo, high: hi });
123 continue;
124 }
125 if hi - lo < 1e-12 {
126 if count == 1 {
127 intervals.push(RootInterval { low: lo, high: hi });
128 }
129 continue;
130 }
131
132 let mid = (lo + hi) / 2.0;
133 stack.push((lo, mid));
134 stack.push((mid, hi));
135 }
136
137 intervals
138 }
139
140 pub fn refine_root(&self, interval: &RootInterval, tol: f64) -> RootInterval {
142 let mut lo = interval.low;
143 let mut hi = interval.high;
144 let f_lo = self.eval_f64(lo);
145
146 if f_lo.abs() < 1e-15 {
147 return RootInterval { low: lo, high: lo };
148 }
149
150 while hi - lo > tol {
151 let mid = (lo + hi) / 2.0;
152 let f_mid = self.eval_f64(mid);
153 if f_mid.abs() < 1e-15 {
154 return RootInterval {
155 low: mid,
156 high: mid,
157 };
158 }
159 if f_lo * f_mid < 0.0 {
160 hi = mid;
161 } else {
162 lo = mid;
163 }
164 }
165
166 RootInterval { low: lo, high: hi }
167 }
168}
169
170fn count_sign_changes_at_infinity<D: EuclideanDomain>(
172 seq: &[DenseUnivariatePolynomial<D>],
173 at_neg_inf: bool,
174) -> usize
175where
176 D::Element: Display,
177{
178 let vals: Vec<f64> = seq
179 .iter()
180 .map(|p| {
181 if p.is_zero() {
182 return 0.0;
183 }
184 let deg = p.degree().unwrap_or(0);
185 let lc = coeff_value(p.leading_coeff().unwrap());
186 if at_neg_inf {
189 if deg % 2 == 0 { lc } else { -lc }
190 } else {
191 lc
192 }
193 })
194 .collect();
195 count_sign_changes_in_vals(&vals)
196}
197
198fn count_sign_changes<D: EuclideanDomain>(seq: &[DenseUnivariatePolynomial<D>], x: f64) -> usize
200where
201 D::Element: Display,
202{
203 let vals: Vec<f64> = seq.iter().map(|p| p.eval_f64(x)).collect();
204 count_sign_changes_in_vals(&vals)
205}
206
207fn count_sign_changes_in_vals(vals: &[f64]) -> usize {
208 let mut count = 0;
209 let mut prev_sign: Option<bool> = None;
210 for &v in vals {
211 if v == 0.0 {
212 continue;
213 }
214 let sign = v > 0.0;
215 if let Some(p) = prev_sign
216 && p != sign
217 {
218 count += 1;
219 }
220 prev_sign = Some(sign);
221 }
222 count
223}
224
225fn root_bound<D: EuclideanDomain>(p: &DenseUnivariatePolynomial<D>) -> f64
227where
228 D::Element: Display,
229{
230 if p.is_zero() || p.degree().is_none() {
231 return 1.0;
232 }
233 let coeffs = p.coeffs();
234 let lc = coeff_value(coeffs.last().unwrap()).abs();
235 let mut max_abs = 0.0f64;
236 for c in &coeffs[..coeffs.len() - 1] {
237 let v = coeff_value(c).abs();
238 if v > max_abs {
239 max_abs = v;
240 }
241 }
242 1.0 + max_abs / lc.max(1e-10)
243}
244
245fn coeff_value(elem: &(impl Display + ?Sized)) -> f64 {
247 let s = elem.to_string();
248 let trimmed = s.trim();
249 if let Ok(v) = trimmed.parse::<f64>() {
251 return v;
252 }
253 if let Ok(v) = trimmed.parse::<i64>() {
255 return v as f64;
256 }
257 if let Some((num_str, den_str)) = trimmed.split_once('/')
259 && let (Ok(n), Ok(d)) = (num_str.trim().parse::<f64>(), den_str.trim().parse::<f64>())
260 && d != 0.0
261 {
262 return n / d;
263 }
264 0.0
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270 use ocas_domain::{Integer, IntegerDomain};
271
272 fn i(n: i64) -> Integer {
273 Integer::from(n)
274 }
275
276 #[test]
277 fn count_roots_x2_minus_1() {
278 let d = IntegerDomain;
279 let p = DenseUnivariatePolynomial::from_coeffs(d, vec![i(-1), i(0), i(1)]);
280 assert_eq!(p.count_real_roots(), 2);
281 }
282
283 #[test]
284 fn count_roots_x2_plus_1() {
285 let d = IntegerDomain;
286 let p = DenseUnivariatePolynomial::from_coeffs(d, vec![i(1), i(0), i(1)]);
287 assert_eq!(p.count_real_roots(), 0);
288 }
289
290 #[test]
291 fn count_roots_perfect_square() {
292 let d = IntegerDomain;
293 let p = DenseUnivariatePolynomial::from_coeffs(d, vec![i(1), i(2), i(1)]);
295 assert_eq!(p.count_real_roots(), 1);
296 }
297
298 #[test]
299 fn isolate_roots_x2_minus_2() {
300 let d = IntegerDomain;
301 let p = DenseUnivariatePolynomial::from_coeffs(d, vec![i(-2), i(0), i(1)]);
302 let intervals = p.isolate_real_roots();
303 assert_eq!(intervals.len(), 2);
304 let refined = p.refine_root(&intervals[1], 1e-6);
306 let approx = (refined.low + refined.high) / 2.0;
307 assert!((approx.abs() - std::f64::consts::SQRT_2).abs() < 0.01);
308 }
309
310 #[test]
311 fn sturm_sequence_length() {
312 let d = IntegerDomain;
313 let p = DenseUnivariatePolynomial::from_coeffs(d, vec![i(-1), i(0), i(1)]);
314 let seq = p.sturm_sequence();
315 assert!(seq.len() >= 2);
316 }
317}