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

Skip to content

Commit d2f34e9

Browse files
Faster bezier root finding on [0, 1]
1 parent eb7c4ce commit d2f34e9

1 file changed

Lines changed: 104 additions & 10 deletions

File tree

lib/matplotlib/bezier.py

Lines changed: 104 additions & 10 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
@lru_cache(maxsize=16)
@@ -309,17 +385,35 @@ def axis_aligned_extrema(self):
309385
if n <= 1:
310386
return np.array([]), np.array([])
311387
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+
315393
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)
323417

324418

325419
def split_bezier_intersecting_with_closedpath(

0 commit comments

Comments
 (0)