qualia_core_db/solvers/
polynomial.rs1use crate::solvers::SolversError;
10
11#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct Complex {
14 pub re: f64,
15 pub im: f64,
16}
17
18impl Complex {
19 pub const fn new(re: f64, im: f64) -> Self {
20 Self { re, im }
21 }
22 pub const fn real(re: f64) -> Self {
23 Self { re, im: 0.0 }
24 }
25
26 #[inline]
27 pub fn add(self, o: Complex) -> Complex {
28 Complex::new(self.re + o.re, self.im + o.im)
29 }
30 #[inline]
31 pub fn sub(self, o: Complex) -> Complex {
32 Complex::new(self.re - o.re, self.im - o.im)
33 }
34 #[inline]
35 pub fn mul(self, o: Complex) -> Complex {
36 Complex::new(
37 self.re * o.re - self.im * o.im,
38 self.re * o.im + self.im * o.re,
39 )
40 }
41 #[inline]
42 pub fn div(self, o: Complex) -> Complex {
43 let d = o.re * o.re + o.im * o.im;
44 Complex::new(
45 (self.re * o.re + self.im * o.im) / d,
46 (self.im * o.re - self.re * o.im) / d,
47 )
48 }
49 #[inline]
51 pub fn abs(self) -> f64 {
52 self.re.hypot(self.im)
53 }
54 #[inline]
56 pub fn is_real(self, tol: f64) -> bool {
57 self.im.abs() <= tol
58 }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq)]
63pub enum QuadraticRoots {
64 TwoReal(f64, f64),
66 DoubleReal(f64),
68 ComplexPair { re: f64, im: f64 },
70 Linear(f64),
72}
73
74pub fn solve_quadratic(a: f64, b: f64, c: f64) -> Result<QuadraticRoots, SolversError> {
81 if !(a.is_finite() && b.is_finite() && c.is_finite()) {
82 return Err(SolversError::ComputationError);
83 }
84 let scale = a.abs().max(b.abs()).max(c.abs()).max(1.0);
85
86 if a.abs() <= f64::EPSILON * scale {
88 if b.abs() <= f64::EPSILON * scale {
89 return Err(SolversError::ComputationError);
90 }
91 return Ok(QuadraticRoots::Linear(-c / b));
92 }
93
94 let disc = b * b - 4.0 * a * c;
95 let disc_scale = (b * b).max((4.0 * a * c).abs()).max(1.0);
96 if disc.abs() <= 1e-12 * disc_scale {
97 return Ok(QuadraticRoots::DoubleReal(-b / (2.0 * a)));
98 }
99
100 if disc > 0.0 {
101 let sqrt_d = disc.sqrt();
102 let sign_b = if b >= 0.0 { 1.0 } else { -1.0 };
103 let q = -0.5 * (b + sign_b * sqrt_d);
104 let r1 = q / a;
105 let r2 = c / q;
106 let (lo, hi) = if r1 <= r2 { (r1, r2) } else { (r2, r1) };
107 Ok(QuadraticRoots::TwoReal(lo, hi))
108 } else {
109 let re = -b / (2.0 * a);
110 let im = (-disc).sqrt() / (2.0 * a.abs());
111 Ok(QuadraticRoots::ComplexPair { re, im })
112 }
113}
114
115fn poly_eval_complex(coeffs: &[f64], x: Complex) -> Complex {
118 let mut acc = Complex::real(0.0);
119 for &c in coeffs {
120 acc = acc.mul(x).add(Complex::real(c));
121 }
122 acc
123}
124
125pub fn polynomial_roots(coeffs: &[f64]) -> Result<Vec<Complex>, SolversError> {
132 let start = coeffs
134 .iter()
135 .position(|c| c.abs() > 0.0)
136 .ok_or(SolversError::ComputationError)?;
137 let coeffs = &coeffs[start..];
138 if coeffs.len() == 1 {
139 return Ok(Vec::new()); }
141 if coeffs.iter().any(|c| !c.is_finite()) {
142 return Err(SolversError::ComputationError);
143 }
144
145 let lead = coeffs[0];
147 let monic: Vec<f64> = coeffs.iter().map(|c| c / lead).collect();
148 let degree = monic.len() - 1;
149
150 let seed = Complex::new(0.4, 0.9);
152 let mut roots: Vec<Complex> = (0..degree)
153 .map(|k| {
154 let mut z = Complex::real(1.0);
155 for _ in 0..k {
156 z = z.mul(seed);
157 }
158 z
159 })
160 .collect();
161
162 const MAX_ITERS: usize = 500;
163 const TOL: f64 = 1e-14;
164 for _ in 0..MAX_ITERS {
165 let mut max_delta = 0.0_f64;
166 for i in 0..degree {
167 let zi = roots[i];
168 let mut denom = Complex::real(1.0);
170 for j in 0..degree {
171 if j != i {
172 denom = denom.mul(zi.sub(roots[j]));
173 }
174 }
175 if denom.abs() == 0.0 {
176 continue; }
178 let delta = poly_eval_complex(&monic, zi).div(denom);
179 roots[i] = zi.sub(delta);
180 max_delta = max_delta.max(delta.abs());
181 }
182 if max_delta < TOL {
183 break;
184 }
185 }
186
187 for r in roots.iter_mut() {
189 if r.im.abs() < 1e-9 * (1.0 + r.re.abs()) {
190 r.im = 0.0;
191 }
192 }
193 Ok(roots)
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn quadratic_two_real() {
202 assert_eq!(
204 solve_quadratic(1.0, -3.0, 2.0).unwrap(),
205 QuadraticRoots::TwoReal(1.0, 2.0)
206 );
207 }
208
209 #[test]
210 fn quadratic_double_and_complex_and_linear() {
211 assert_eq!(
213 solve_quadratic(1.0, -2.0, 1.0).unwrap(),
214 QuadraticRoots::DoubleReal(1.0)
215 );
216 match solve_quadratic(1.0, 0.0, 1.0).unwrap() {
218 QuadraticRoots::ComplexPair { re, im } => {
219 assert!(re.abs() < 1e-12 && (im - 1.0).abs() < 1e-12);
220 }
221 other => panic!("expected complex pair, got {other:?}"),
222 }
223 assert_eq!(
225 solve_quadratic(0.0, 2.0, 4.0).unwrap(),
226 QuadraticRoots::Linear(-2.0)
227 );
228 }
229
230 #[test]
231 fn quadratic_rejects_degenerate() {
232 assert!(matches!(
233 solve_quadratic(0.0, 0.0, 1.0),
234 Err(SolversError::ComputationError)
235 ));
236 assert!(matches!(
237 solve_quadratic(f64::NAN, 1.0, 1.0),
238 Err(SolversError::ComputationError)
239 ));
240 }
241
242 #[test]
243 fn roots_of_known_polynomial() {
244 let roots = polynomial_roots(&[1.0, -6.0, 11.0, -6.0]).unwrap();
246 assert_eq!(roots.len(), 3);
247 let mut reals: Vec<f64> = roots.iter().map(|r| r.re).collect();
248 reals.sort_by(|a, b| a.partial_cmp(b).unwrap());
249 for (got, want) in reals.iter().zip([1.0, 2.0, 3.0]) {
250 assert!((got - want).abs() < 1e-6, "{got} != {want}");
251 }
252 assert!(roots.iter().all(|r| r.is_real(1e-6)));
253 }
254
255 #[test]
256 fn roots_complex_pair() {
257 let roots = polynomial_roots(&[1.0, 0.0, 1.0]).unwrap();
259 assert_eq!(roots.len(), 2);
260 assert!(roots.iter().any(|r| (r.im.abs() - 1.0).abs() < 1e-6));
261 }
262
263 #[test]
264 fn roots_reject_zero_polynomial() {
265 assert!(matches!(
266 polynomial_roots(&[0.0, 0.0]),
267 Err(SolversError::ComputationError)
268 ));
269 }
270}