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

Skip to content

Commit 350fc98

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

1 file changed

Lines changed: 103 additions & 10 deletions

File tree

lib/matplotlib/bezier.py

Lines changed: 103 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,81 @@
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(f, a, b, tol=1e-12, max_iter=54):
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+
for i in sign_changes:
49+
roots.append(_bisect(f, ts[i], ts[i + 1]))
50+
51+
# Check endpoints
52+
if abs(vals[0]) < 1e-12:
53+
roots.insert(0, 0.0)
54+
if abs(vals[-1]) < 1e-12 and (not roots or abs(roots[-1] - 1.0) > 1e-10):
55+
roots.append(1.0)
56+
57+
return np.asarray(roots)
58+
59+
60+
def _quadratic_roots_in_01(c0, c1, c2):
61+
"""Real roots of c0 + c1*x + c2*x**2 in [0, 1]."""
62+
if abs(c2) < 1e-12: # Linear
63+
if abs(c1) < 1e-12:
64+
return np.array([])
65+
root = -c0 / c1
66+
return np.array([root]) if 0 <= root <= 1 else np.array([])
67+
68+
disc = c1 * c1 - 4 * c2 * c0
69+
if disc < 0:
70+
return np.array([])
71+
72+
sqrt_disc = np.sqrt(disc)
73+
# Numerically stable quadratic formula
74+
if c1 >= 0:
75+
q = -0.5 * (c1 + sqrt_disc)
76+
else:
77+
q = -0.5 * (c1 - sqrt_disc)
78+
79+
roots = []
80+
if abs(q) > 1e-12:
81+
roots.append(c0 / q)
82+
if abs(c2) > 1e-12:
83+
roots.append(q / c2)
84+
85+
roots = np.asarray(roots)
86+
return roots[(roots >= 0) & (roots <= 1)]
1287

1388

1489
# same algorithm as 3.8's math.comb
@@ -322,17 +397,35 @@ def axis_aligned_extrema(self):
322397
if n <= 1:
323398
return np.array([]), np.array([])
324399
Cj = self.polynomial_coefficients
325-
dCj = np.arange(1, n+1)[:, None] * Cj[1:]
326-
dims = []
327-
roots = []
400+
dCj = np.arange(1, n + 1)[:, None] * Cj[1:]
401+
402+
all_dims = []
403+
all_roots = []
404+
328405
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]
406+
# Trim trailing near-zeros to get actual degree
407+
deg = len(pi) - 1
408+
while deg > 0 and abs(pi[deg]) < 1e-12:
409+
deg -= 1
410+
411+
if deg == 0:
412+
continue
413+
elif deg == 1:
414+
root = -pi[0] / pi[1]
415+
r = np.array([root]) if 0 <= root <= 1 else np.array([])
416+
elif deg == 2:
417+
r = _quadratic_roots_in_01(pi[0], pi[1], pi[2])
418+
else:
419+
r = _real_roots_in_01(pi[:deg + 1])
420+
421+
if len(r) > 0:
422+
all_roots.append(r)
423+
all_dims.append(np.full(len(r), i))
424+
425+
if not all_roots:
426+
return np.array([]), np.array([])
427+
428+
return np.concatenate(all_dims), np.concatenate(all_roots)
336429

337430

338431
def split_bezier_intersecting_with_closedpath(

0 commit comments

Comments
 (0)