|
11 | 11 | from matplotlib import _api |
12 | 12 |
|
13 | 13 |
|
| 14 | +def _quadratic_roots_in_01(c0, c1, c2): |
| 15 | + """Real roots of c0 + c1*x + c2*x**2 in [0, 1].""" |
| 16 | + if abs(c2) < 1e-12: # Linear |
| 17 | + if abs(c1) < 1e-12: |
| 18 | + return np.array([]) |
| 19 | + root = -c0 / c1 |
| 20 | + return np.array([root]) if 0 <= root <= 1 else np.array([]) |
| 21 | + |
| 22 | + disc = c1 * c1 - 4 * c2 * c0 |
| 23 | + if disc < 0: |
| 24 | + return np.array([]) |
| 25 | + |
| 26 | + sqrt_disc = np.sqrt(disc) |
| 27 | + # Numerically stable quadratic formula |
| 28 | + if c1 >= 0: |
| 29 | + q = -0.5 * (c1 + sqrt_disc) |
| 30 | + else: |
| 31 | + q = -0.5 * (c1 - sqrt_disc) |
| 32 | + |
| 33 | + roots = [] |
| 34 | + if abs(q) > 1e-12: |
| 35 | + roots.append(c0 / q) |
| 36 | + if abs(c2) > 1e-12: |
| 37 | + roots.append(q / c2) |
| 38 | + |
| 39 | + roots = np.asarray(roots) |
| 40 | + return roots[(roots >= 0) & (roots <= 1)] |
| 41 | + |
| 42 | + |
| 43 | +def _real_roots_in_01(coeffs): |
| 44 | + """ |
| 45 | + Find real roots of a polynomial in the interval [0, 1]. |
| 46 | +
|
| 47 | + For polynomials of degree <= 2, closed-form solutions are used. |
| 48 | + For higher degrees, `numpy.roots` is used as a fallback. In practice, |
| 49 | + matplotlib only ever uses cubic bezier curves and axis_aligned_extrema() |
| 50 | + differentiates, so we only ever find roots for degree <= 2. |
| 51 | +
|
| 52 | + Parameters |
| 53 | + ---------- |
| 54 | + coeffs : array-like |
| 55 | + Polynomial coefficients in ascending order: |
| 56 | + ``c[0] + c[1]*x + c[2]*x**2 + ...`` |
| 57 | + Note this is the opposite convention from `numpy.roots`. |
| 58 | +
|
| 59 | + Returns |
| 60 | + ------- |
| 61 | + roots : ndarray |
| 62 | + Sorted array of real roots in [0, 1]. |
| 63 | + """ |
| 64 | + coeffs = np.asarray(coeffs, dtype=float) |
| 65 | + |
| 66 | + # Trim trailing near-zeros to get actual degree |
| 67 | + deg = len(coeffs) - 1 |
| 68 | + while deg > 0 and abs(coeffs[deg]) < 1e-12: |
| 69 | + deg -= 1 |
| 70 | + |
| 71 | + if deg <= 0: |
| 72 | + return np.array([]) |
| 73 | + elif deg == 1: |
| 74 | + root = -coeffs[0] / coeffs[1] |
| 75 | + return np.array([root]) if 0 <= root <= 1 else np.array([]) |
| 76 | + elif deg == 2: |
| 77 | + roots = _quadratic_roots_in_01(coeffs[0], coeffs[1], coeffs[2]) |
| 78 | + else: |
| 79 | + # np.roots expects descending order (highest power first) |
| 80 | + eps = 1e-10 |
| 81 | + all_roots = np.roots(coeffs[deg::-1]) |
| 82 | + real_mask = np.abs(all_roots.imag) < eps |
| 83 | + real_roots = all_roots[real_mask].real |
| 84 | + in_range = (real_roots >= -eps) & (real_roots <= 1 + eps) |
| 85 | + roots = np.clip(real_roots[in_range], 0, 1) |
| 86 | + |
| 87 | + return np.sort(roots) |
| 88 | + |
| 89 | + |
14 | 90 | @lru_cache(maxsize=16) |
15 | 91 | def _get_coeff_matrix(n): |
16 | 92 | """ |
@@ -309,17 +385,21 @@ 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 | + r = _real_roots_in_01(pi) |
| 395 | + if len(r) > 0: |
| 396 | + all_roots.append(r) |
| 397 | + all_dims.append(np.full(len(r), i)) |
| 398 | + |
| 399 | + if not all_roots: |
| 400 | + return np.array([]), np.array([]) |
| 401 | + |
| 402 | + return np.concatenate(all_dims), np.concatenate(all_roots) |
323 | 403 |
|
324 | 404 |
|
325 | 405 | def split_bezier_intersecting_with_closedpath( |
|
0 commit comments