Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 95c8fcb

Browse files
Faster bezier root finding on [0, 1]
1 parent 74fa286 commit 95c8fcb

1 file changed

Lines changed: 105 additions & 11 deletions

File tree

lib/matplotlib/bezier.py

Lines changed: 105 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,82 @@
99
import numpy as np
1010

1111
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)]
1288

1389

1490
# same algorithm as 3.8's math.comb
@@ -22,7 +98,7 @@ def _comb(n, k):
2298
return np.prod((n + 1 - i)/i).astype(int)
2399

24100

25-
# Precomputed matrices for converting Bézier control points to polynomial
101+
# Precomputed matrices for converting Bezier control points to polynomial
26102
# coefficients. _COEFF_MATRICES[n] @ control_points gives coefficients.
27103
# These avoid the slow _comb vectorized function for common cases.
28104
_COEFF_MATRICES = {
@@ -322,17 +398,35 @@ def axis_aligned_extrema(self):
322398
if n <= 1:
323399
return np.array([]), np.array([])
324400
Cj = self.polynomial_coefficients
325-
dCj = np.arange(1, n+1)[:, None] * Cj[1:]
326-
dims = []
327-
roots = []
401+
dCj = np.arange(1, n + 1)[:, None] * Cj[1:]
402+
403+
all_dims = []
404+
all_roots = []
405+
328406
for i, pi in enumerate(dCj.T):
329-
r = np.roots(pi[::-1])
330-
roots.append(r)
331-
dims.append(np.full_like(r, i))
332-
roots = np.concatenate(roots)
333-
dims = np.concatenate(dims)
334-
in_range = np.isreal(roots) & (roots >= 0) & (roots <= 1)
335-
return dims[in_range], np.real(roots)[in_range]
407+
# Trim trailing near-zeros to get actual degree
408+
deg = len(pi) - 1
409+
while deg > 0 and abs(pi[deg]) < 1e-12:
410+
deg -= 1
411+
412+
if deg == 0:
413+
continue
414+
elif deg == 1:
415+
root = -pi[0] / pi[1]
416+
r = np.array([root]) if 0 <= root <= 1 else np.array([])
417+
elif deg == 2:
418+
r = _quadratic_roots_in_01(pi[0], pi[1], pi[2])
419+
else:
420+
r = _real_roots_in_01(pi[:deg + 1])
421+
422+
if len(r) > 0:
423+
all_roots.append(r)
424+
all_dims.append(np.full(len(r), i))
425+
426+
if not all_roots:
427+
return np.array([]), np.array([])
428+
429+
return np.concatenate(all_dims), np.concatenate(all_roots)
336430

337431

338432
def split_bezier_intersecting_with_closedpath(

0 commit comments

Comments
 (0)