1use arrayvec::ArrayVec;
2
3use crate::{Cubic, Quadratic, different_signs};
4
5#[cfg(feature = "libm")]
6#[allow(unused_imports, reason = "unused if libm and std are both around")]
7use crate::libm_polyfill::FloatFuncs as _;
8
9fn partial_sort<const M: usize>(buf: &mut ArrayVec<f64, M>) {
15 if buf.len() > 1 && buf[0] > buf[1] {
16 buf.swap(0, 1);
17 if buf.len() > 2 && buf[1] > buf[2] {
18 buf.swap(1, 2);
19 }
20 }
21}
22
23impl Cubic {
24 #[doc(hidden)]
29 pub fn eval_opt(&self, x: f64) -> f64 {
30 let [c0, c1, c2, c3] = self.coeffs;
31 let xx = x * x;
32 let xxx = xx * x;
33 c0 + c1 * x + c2 * xx + c3 * xxx
34 }
35
36 #[doc(hidden)]
44 pub fn eval_with_deriv_opt(&self, deriv: &Quadratic, x: f64) -> (f64, f64) {
45 let [c0, c1, c2, c3] = self.coeffs;
46 let [d0, d1, d2] = deriv.coeffs;
47 let xx = x * x;
48 let xxx = xx * x;
49 (c0 + c1 * x + c2 * xx + c3 * xxx, d0 + d1 * x + d2 * xx)
50 }
51
52 fn critical_points(&self) -> Option<(f64, f64)> {
63 let a = 3.0 * self.coeffs[3];
64 let b_2 = self.coeffs[2];
65 let c = self.coeffs[1];
66 let disc_4 = b_2 * b_2 - a * c;
67
68 if !disc_4.is_finite() {
69 return self.rescaled_critical_points();
70 }
71
72 if disc_4 > 0.0 {
73 let q = -(b_2 + disc_4.sqrt().copysign(b_2));
74 let r0 = q / a;
75 let r1 = c / q;
76 Some((r0.min(r1), r0.max(r1)))
77 } else {
78 None
79 }
80 }
81
82 #[cold]
83 fn rescaled_critical_points(&self) -> Option<(f64, f64)> {
84 let scale = 2.0f64.powi(-515);
85 (*self * scale).critical_points()
86 }
87
88 fn one_root(
89 &self,
90 lower: f64,
91 upper: f64,
92 lower_val: f64,
93 upper_val: f64,
94 x_error: f64,
95 ) -> f64 {
96 let deriv = self.deriv();
97 if !deriv.is_finite() {
98 return f64::NAN;
99 }
100 crate::yuksel::find_root(
101 |x| self.eval_opt(x),
102 |x| deriv.eval_opt(x),
103 lower,
104 upper,
105 lower_val,
106 upper_val,
107 x_error,
108 )
109 }
110
111 #[doc(hidden)]
113 pub fn root_between(self, lower: f64, upper: f64, x_error: f64) -> f64 {
114 self.one_root(lower, upper, self.eval(lower), self.eval(upper), x_error)
115 }
116
117 fn first_root(self, lower: f64, upper: f64, x_error: f64) -> Option<f64> {
118 if let Some((x0, x1)) = self.critical_points() {
119 let possible_endpoints: [f64; 3] = [x0, x1, upper];
120 let mut last = lower;
121 let mut last_val = self.eval(last);
122 for x in possible_endpoints {
123 if x > last && x <= upper {
124 let val = self.eval(x);
125 if different_signs(last_val, val) {
126 return Some(self.one_root(last, x, last_val, val, x_error));
127 }
128
129 last = x;
130 last_val = val;
131 }
132 }
133 None
134 } else {
135 let lower_val = self.eval(lower);
136 let upper_val = self.eval(upper);
137 if different_signs(lower_val, upper_val) {
138 Some(self.one_root(lower, upper, lower_val, upper_val, x_error))
139 } else {
140 None
141 }
142 }
143 }
144
145 pub fn roots_between(self, lower: f64, upper: f64, x_error: f64) -> ArrayVec<f64, 3> {
153 let mut ret = ArrayVec::new();
154 let mut scratch = ArrayVec::new();
155 self.roots_between_with_buffer(lower, upper, x_error, &mut scratch, &mut ret);
156 ret
157 }
158
159 pub(crate) fn roots_between_with_buffer<const M: usize>(
160 self,
161 lower: f64,
162 upper: f64,
163 x_error: f64,
164 _scratch: &mut ArrayVec<f64, M>,
165 out: &mut ArrayVec<f64, M>,
166 ) {
167 if let Some(r) = self.first_root(lower, upper, x_error) {
168 out.push(r);
169 let quad = self.deflate(r);
170 if let Some((x0, x1)) = quad.positive_discriminant_roots() {
171 if lower <= x0 && x0 <= upper {
172 out.push(x0);
173 }
174 if lower <= x1 && x1 <= upper {
175 out.push(x1);
176 }
177
178 if lower <= x0 && x0 < r {
182 partial_sort(out);
183 }
184 }
185 }
186 }
187
188 #[doc(hidden)]
189 pub fn precondition(&self) -> Cubic {
190 let min_coeff = 2.0f64.powi(-256);
193 let truncate = |x: &mut f64| {
194 if x.abs() <= min_coeff {
195 *x = 0.0
196 }
197 };
198
199 let small_coeff = 2.0f64.powi(-64);
203
204 let large_coeff = 2.0f64.powi(64);
205
206 let mut c = *self;
207 if (self.magnitude() != 0.0 && self.magnitude() <= small_coeff)
208 || self.magnitude() >= large_coeff
209 {
210 c /= self.magnitude();
211 }
212
213 truncate(&mut c.coeffs[0]);
214 truncate(&mut c.coeffs[1]);
215 truncate(&mut c.coeffs[2]);
216 truncate(&mut c.coeffs[3]);
217 c
218 }
219
220 #[doc(hidden)]
225 pub fn roots_blinn(&self) -> ArrayVec<f64, 3> {
226 let mut ret = ArrayVec::new();
227 let a = self.coeffs[3];
228 let b = self.coeffs[2] * (1.0 / 3.0);
229 let c = self.coeffs[1] * (1.0 / 3.0);
230 let d = self.coeffs[0];
231
232 let delta_1 = a * c - b * b;
233 let delta_2 = a * d - b * c;
234 let delta_3 = b * d - c * c;
235 let disc = 4.0 * delta_1 * delta_3 - delta_2 * delta_2;
236
237 if !disc.is_finite() {
238 return ret;
239 }
240 let mut push = |x: f64| {
248 if x.is_finite() {
249 ret.push(x);
250 }
251 };
252 if disc <= 0.0 {
253 let (tilde_a, tilde_c, tilde_d) = if b * b * b * d >= a * c * c * c {
255 (a, delta_1, -2.0 * b * delta_1 + a * delta_2)
256 } else {
257 (d, delta_3, -d * delta_2 + 2.0 * c * delta_3)
258 };
259 let t_0 = -tilde_a.copysign(tilde_d) * (-disc).sqrt();
261 let t_1 = -tilde_d + t_0;
262 let p = (t_1 / 2.0).cbrt();
263
264 let q = if t_0 == t_1 { -p } else { -tilde_c / p };
265 let tilde_x = if tilde_c <= 0.0 {
267 p + q
268 } else {
269 -tilde_d / (p * p + q * q + tilde_c)
270 };
271
272 let (x, w) = if b * b * b * d >= a * c * c * c {
273 (tilde_x - b, a)
274 } else {
275 (-d, tilde_x + c)
276 };
277
278 push(x / w);
279 } else {
280 fn one_root(a_or_d: f64, disc: f64, bar_c: f64, bar_d: f64) -> (f64, f64) {
282 let sqrt_c = (-bar_c).sqrt();
283 let theta = (1.0 / 3.0) * (a_or_d * disc.sqrt()).atan2(-bar_d).abs();
284 let (sin_theta, cos_theta) = theta.sin_cos();
285 let tilde_x_1 = 2.0 * sqrt_c * cos_theta;
287 let tilde_x_3 = sqrt_c * (-cos_theta - 3.0f64.sqrt() * sin_theta);
288 (tilde_x_1, tilde_x_3)
289 }
290
291 let bar_c_a = delta_1;
292 let bar_d_a = -2.0 * b * delta_1 + a * delta_2;
293 let (tilde_x_1_a, tilde_x_3_a) = one_root(a, disc, bar_c_a, bar_d_a);
294
295 let bar_c_d = delta_3;
296 let bar_d_d = -d * delta_2 + 2.0 * c * delta_3;
297 let (tilde_x_1_d, tilde_x_3_d) = one_root(d, disc, bar_c_d, bar_d_d);
298
299 let tilde_x_l = if tilde_x_1_a + tilde_x_3_a > 2.0 * b {
300 tilde_x_1_a
301 } else {
302 tilde_x_3_a
303 };
304 let tilde_x_s = if tilde_x_1_d + tilde_x_3_d < 2.0 * c {
305 tilde_x_1_d
306 } else {
307 tilde_x_3_d
308 };
309
310 let (x_l, w_l) = (tilde_x_l - b, a);
311 let (x_s, w_s) = (-d, tilde_x_s + c);
312
313 let e = w_l * w_s;
314 let f = -x_l * w_s - w_l * x_s;
315 let g = x_l * x_s;
316
317 let (x_m, w_m) = (c * f - b * g, c * e - b * f);
318
319 push(x_l / w_l);
320 push(x_s / w_s);
321 push(x_m / w_m);
322 }
323 ret
324 }
325
326 #[doc(hidden)]
331 pub fn roots_blinn_and_deflate(&self) -> ArrayVec<f64, 3> {
332 let mut ret = ArrayVec::new();
333 let a = self.coeffs[3];
334 let b = self.coeffs[2] * (1.0 / 3.0);
335 let c = self.coeffs[1] * (1.0 / 3.0);
336 let d = self.coeffs[0];
337
338 let delta_1 = a * c - b * b;
339 let delta_2 = a * d - b * c;
340 let delta_3 = b * d - c * c;
341 let disc = 4.0 * delta_1 * delta_3 - delta_2 * delta_2;
342
343 if !disc.is_finite() {
344 return ret;
345 }
346 if disc <= 0.0 {
350 let (tilde_a, tilde_c, tilde_d) = if b * b * b * d >= a * c * c * c {
351 (a, delta_1, -2.0 * b * delta_1 + a * delta_2)
352 } else {
353 (d, delta_3, -d * delta_2 + 2.0 * c * delta_3)
354 };
355 let t_0 = -tilde_a.copysign(tilde_d) * (-disc).sqrt();
356 let t_1 = -tilde_d + t_0;
357 let p = (t_1 / 2.0).cbrt();
358
359 let q = if t_0 == t_1 { -p } else { -tilde_c / p };
360 let tilde_x = if tilde_c <= 0.0 {
361 p + q
362 } else {
363 -tilde_d / (p * p + q * q + tilde_c)
364 };
365
366 let (x, w) = if b * b * b * d >= a * c * c * c {
367 (tilde_x - b, a)
368 } else {
369 (-d, tilde_x + c)
370 };
371
372 if x.is_finite() && w.is_finite() {
373 ret.push(x / w);
374 }
375 } else {
376 fn one_root(a_or_d: f64, disc: f64, bar_c: f64, bar_d: f64) -> (f64, f64) {
377 let sqrt_c = (-bar_c).sqrt();
378 let theta = (1.0 / 3.0) * (a_or_d * disc.sqrt()).atan2(-bar_d).abs();
379 let (sin_theta, cos_theta) = theta.sin_cos();
380 let tilde_x_1 = 2.0 * sqrt_c * cos_theta;
381 let tilde_x_3 = sqrt_c * (-theta.cos() - 3.0f64.sqrt() * sin_theta);
382 (tilde_x_1, tilde_x_3)
383 }
384
385 let bar_c_a = delta_1;
388 let bar_d_a = -2.0 * b * delta_1 + a * delta_2;
389 let (tilde_x_1_a, tilde_x_3_a) = one_root(a, disc, bar_c_a, bar_d_a);
390
391 let bar_c_d = delta_3;
392 let bar_d_d = -d * delta_2 + 2.0 * c * delta_3;
393 let (tilde_x_1_d, tilde_x_3_d) = one_root(d, disc, bar_c_d, bar_d_d);
394
395 let tilde_x_l = if tilde_x_1_a + tilde_x_3_a > 2.0 * b {
396 tilde_x_1_a
397 } else {
398 tilde_x_3_a
399 };
400 let tilde_x_s = if tilde_x_1_d + tilde_x_3_d < 2.0 * c {
402 tilde_x_1_d
403 } else {
404 tilde_x_3_d
405 };
406
407 let (x_l, w_l) = (tilde_x_l - b, a);
408 let (x_s, w_s) = (-d, tilde_x_s + c);
409
410 let x = if (x_l * w_s).abs() <= (x_s * w_l).abs() {
411 x_l / w_l
412 } else {
413 x_s / w_s
414 };
415 if x.is_finite() {
416 ret.push(x);
417 }
418 let q = self.deflate(x);
419 if q.is_finite() {
421 ret.extend(q.roots());
422 partial_sort(&mut ret);
423 }
424 }
425 ret
426 }
427}
428
429#[cfg(test)]
430mod tests {
431 use crate::Cubic;
432
433 const TRICKY_CUBICS: [Cubic; 5] = [
434 Cubic::new([
436 1.6149620090145706e-94,
437 1.6149620090145634e-94,
438 1.6149620090145663e-94,
439 9.66803867245343e272,
440 ]),
441 Cubic::new([
446 -6.323283382275869e98,
447 3.0957754283429482e-307,
448 3.095775428342964e-307,
449 3.095775428342951e-307,
450 ]),
451 Cubic::new([
454 -8.522348907129e-161,
455 4.471145208374078e-67,
456 -0.052026185927646074,
457 -2.9441090045938734e-57,
458 ]),
459 Cubic::new([
462 -2.5162489269306657e-175,
463 -2.516248926930655e-175,
464 -2.5162489269306522e-175,
465 -0.39205037382350466,
466 ]),
467 Cubic::new([
468 -6.428720163649757e103,
469 -6.428720163649766e103,
470 -3.3646756114322413e-74,
471 -3.3646756114322547e-74,
472 ]),
473 ];
474
475 #[test]
476 fn smoke() {
477 let poly = Cubic::new([
500 -6.428720163649757e103,
501 -6.428720163649766e103,
502 -3.3646756114322413e-74,
503 -3.3646756114322547e-74,
504 ]);
505
506 let roots = poly.precondition().roots_blinn();
507 dbg!(&roots);
509 for r in roots {
510 dbg!(poly.eval(r));
511 }
512 }
513
514 #[test]
515 fn bad_for_blinn() {
516 for c in TRICKY_CUBICS {
517 dbg!(c.roots_blinn());
518 dbg!(c.roots_blinn_and_deflate());
519 }
520 }
521
522 fn check_root_values(c: &Cubic, roots: &[f64]) {
526 let magnitude = c.magnitude().max(1.0);
529 let accuracy = magnitude * 1e-12;
530
531 for r in roots {
532 let accuracy = accuracy * r.abs().powi(3).max(1.0);
536 let y = c.eval(*r);
537 if y.is_finite() {
538 assert!(
539 y.abs() <= accuracy,
540 "cubic {c:?} had root {r} evaluate to {y:?}, but expected {accuracy:?}"
541 );
542 }
543 }
544 }
545
546 #[test]
547 fn root_evaluation() {
548 arbtest::arbtest(|u| {
549 let c = crate::arbitrary::cubic(u)?;
550
551 let roots = c.roots_between(-10.0, 10.0, 1e-13);
555 if roots.iter().all(|r| r.is_finite()) {
556 assert!(roots.is_sorted());
557 }
558 check_root_values(&c, &roots);
559
560 let preconditioned = c.precondition();
561 check_root_values(&c, &preconditioned.roots_blinn());
562 check_root_values(&c, &preconditioned.roots_blinn_and_deflate());
563
564 Ok(())
570 })
571 .budget_ms(5_000);
572 }
573
574 #[test]
575 #[ignore]
576 fn root_evaluation_kurbo() {
577 arbtest::arbtest(|u| {
578 let c = crate::arbitrary::cubic(u)?;
579 let magnitude = c.magnitude().max(1.0);
582 let accuracy = magnitude * 1e-12;
583
584 let &[c0, c1, c2, c3] = c.coeffs();
588 for r in kurbo::common::solve_cubic(c0, c1, c2, c3) {
589 let y = c.eval(r);
590 if y.is_finite() {
591 assert!(y.abs() <= accuracy);
592 }
593 }
594 Ok(())
595 })
596 .budget_ms(5_000);
597 }
598}