|
9 | 9 | import numpy as np |
10 | 10 |
|
11 | 11 | from matplotlib import _api |
| 12 | +from numpy.polynomial.polynomial import polyval as _polyval |
| 13 | + |
| 14 | + |
| 15 | +def _bisect_root_finder(f, a, b, tol=1e-12, max_iter=64): |
| 16 | + """Find root of f in [a, b] using bisection. Assumes sign change exists.""" |
| 17 | + fa = f(a) |
| 18 | + for _ in range(max_iter): |
| 19 | + mid = (a + b) * 0.5 |
| 20 | + fm = f(mid) |
| 21 | + if abs(fm) < tol or (b - a) < tol: |
| 22 | + return mid |
| 23 | + if fa * fm < 0: |
| 24 | + b = mid |
| 25 | + else: |
| 26 | + a, fa = mid, fm |
| 27 | + return (a + b) * 0.5 |
| 28 | + |
| 29 | + |
| 30 | +def _real_roots_in_01(coeffs): |
| 31 | + """ |
| 32 | + Find real roots of polynomial in [0, 1] using sampling and bisection. |
| 33 | + coeffs in ascending order: c0 + c1*x + c2*x**2 + ... |
| 34 | + """ |
| 35 | + deg = len(coeffs) - 1 |
| 36 | + n_samples = max(8, deg * 2) |
| 37 | + ts = np.linspace(0, 1, n_samples) |
| 38 | + vals = _polyval(ts, coeffs) |
| 39 | + |
| 40 | + signs = np.sign(vals) |
| 41 | + sign_changes = np.where((signs[:-1] != signs[1:]) & (signs[:-1] != 0))[0] |
| 42 | + |
| 43 | + roots = [] |
| 44 | + |
| 45 | + def f(t): |
| 46 | + return _polyval(t, coeffs) |
| 47 | + |
| 48 | + max_iter = 53 # float64 fractional precision for [0, 1] interval |
| 49 | + for i in sign_changes: |
| 50 | + roots.append(_bisect_root_finder(f, ts[i], ts[i + 1], max_iter=max_iter)) |
| 51 | + |
| 52 | + # Check endpoints |
| 53 | + if abs(vals[0]) < 1e-12: |
| 54 | + roots.insert(0, 0.0) |
| 55 | + if abs(vals[-1]) < 1e-12 and (not roots or abs(roots[-1] - 1.0) > 1e-10): |
| 56 | + roots.append(1.0) |
| 57 | + |
| 58 | + return np.asarray(roots) |
| 59 | + |
| 60 | + |
| 61 | +def _quadratic_roots_in_01(c0, c1, c2): |
| 62 | + """Real roots of c0 + c1*x + c2*x**2 in [0, 1].""" |
| 63 | + if abs(c2) < 1e-12: # Linear |
| 64 | + if abs(c1) < 1e-12: |
| 65 | + return np.array([]) |
| 66 | + root = -c0 / c1 |
| 67 | + return np.array([root]) if 0 <= root <= 1 else np.array([]) |
| 68 | + |
| 69 | + disc = c1 * c1 - 4 * c2 * c0 |
| 70 | + if disc < 0: |
| 71 | + return np.array([]) |
| 72 | + |
| 73 | + sqrt_disc = np.sqrt(disc) |
| 74 | + # Numerically stable quadratic formula |
| 75 | + if c1 >= 0: |
| 76 | + q = -0.5 * (c1 + sqrt_disc) |
| 77 | + else: |
| 78 | + q = -0.5 * (c1 - sqrt_disc) |
| 79 | + |
| 80 | + roots = [] |
| 81 | + if abs(q) > 1e-12: |
| 82 | + roots.append(c0 / q) |
| 83 | + if abs(c2) > 1e-12: |
| 84 | + roots.append(q / c2) |
| 85 | + |
| 86 | + roots = np.asarray(roots) |
| 87 | + return roots[(roots >= 0) & (roots <= 1)] |
12 | 88 |
|
13 | 89 |
|
14 | 90 | @lru_cache(maxsize=16) |
@@ -309,17 +385,35 @@ def axis_aligned_extrema(self): |
309 | 385 | if n <= 1: |
310 | 386 | return np.array([]), np.array([]) |
311 | 387 | Cj = self.polynomial_coefficients |
312 | | - dCj = np.arange(1, n+1)[:, None] * Cj[1:] |
313 | | - dims = [] |
314 | | - roots = [] |
| 388 | + dCj = np.arange(1, n + 1)[:, None] * Cj[1:] |
| 389 | + |
| 390 | + all_dims = [] |
| 391 | + all_roots = [] |
| 392 | + |
315 | 393 | for i, pi in enumerate(dCj.T): |
316 | | - r = np.roots(pi[::-1]) |
317 | | - roots.append(r) |
318 | | - dims.append(np.full_like(r, i)) |
319 | | - roots = np.concatenate(roots) |
320 | | - dims = np.concatenate(dims) |
321 | | - in_range = np.isreal(roots) & (roots >= 0) & (roots <= 1) |
322 | | - return dims[in_range], np.real(roots)[in_range] |
| 394 | + # Trim trailing near-zeros to get actual degree |
| 395 | + deg = len(pi) - 1 |
| 396 | + while deg > 0 and abs(pi[deg]) < 1e-12: |
| 397 | + deg -= 1 |
| 398 | + |
| 399 | + if deg == 0: |
| 400 | + continue |
| 401 | + elif deg == 1: |
| 402 | + root = -pi[0] / pi[1] |
| 403 | + r = np.array([root]) if 0 <= root <= 1 else np.array([]) |
| 404 | + elif deg == 2: |
| 405 | + r = _quadratic_roots_in_01(pi[0], pi[1], pi[2]) |
| 406 | + else: |
| 407 | + r = _real_roots_in_01(pi[:deg + 1]) |
| 408 | + |
| 409 | + if len(r) > 0: |
| 410 | + all_roots.append(r) |
| 411 | + all_dims.append(np.full(len(r), i)) |
| 412 | + |
| 413 | + if not all_roots: |
| 414 | + return np.array([]), np.array([]) |
| 415 | + |
| 416 | + return np.concatenate(all_dims), np.concatenate(all_roots) |
323 | 417 |
|
324 | 418 |
|
325 | 419 | def split_bezier_intersecting_with_closedpath( |
|
0 commit comments