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

Skip to content

Commit 4206313

Browse files
Break out bezier root finding and test it
1 parent d2f34e9 commit 4206313

2 files changed

Lines changed: 94 additions & 17 deletions

File tree

lib/matplotlib/bezier.py

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def _bisect_root_finder(f, a, b, tol=1e-12, max_iter=64):
2727
return (a + b) * 0.5
2828

2929

30-
def _real_roots_in_01(coeffs):
30+
def _bisected_roots_in_01(coeffs):
3131
"""
3232
Find real roots of polynomial in [0, 1] using sampling and bisection.
3333
coeffs in ascending order: c0 + c1*x + c2*x**2 + ...
@@ -87,6 +87,47 @@ def _quadratic_roots_in_01(c0, c1, c2):
8787
return roots[(roots >= 0) & (roots <= 1)]
8888

8989

90+
def _real_roots_in_01(coeffs):
91+
"""
92+
Find real roots of a polynomial in the interval [0, 1].
93+
94+
This is optimized for finding roots only in [0, 1], which is faster than
95+
computing all roots with `numpy.roots` and filtering. For polynomials of
96+
degree <= 2, closed-form solutions are used. For higher degrees, sampling
97+
and bisection are used.
98+
99+
Parameters
100+
----------
101+
coeffs : array-like
102+
Polynomial coefficients in ascending order:
103+
``c[0] + c[1]*x + c[2]*x**2 + ...``
104+
Note this is the opposite convention from `numpy.roots`.
105+
106+
Returns
107+
-------
108+
roots : ndarray
109+
Sorted array of real roots in [0, 1].
110+
"""
111+
coeffs = np.asarray(coeffs, dtype=float)
112+
113+
# Trim trailing near-zeros to get actual degree
114+
deg = len(coeffs) - 1
115+
while deg > 0 and abs(coeffs[deg]) < 1e-12:
116+
deg -= 1
117+
118+
if deg <= 0:
119+
return np.array([])
120+
elif deg == 1:
121+
root = -coeffs[0] / coeffs[1]
122+
return np.array([root]) if 0 <= root <= 1 else np.array([])
123+
elif deg == 2:
124+
roots = _quadratic_roots_in_01(coeffs[0], coeffs[1], coeffs[2])
125+
else:
126+
roots = _bisected_roots_in_01(coeffs[:deg + 1])
127+
128+
return np.sort(roots)
129+
130+
90131
@lru_cache(maxsize=16)
91132
def _get_coeff_matrix(n):
92133
"""
@@ -391,21 +432,7 @@ def axis_aligned_extrema(self):
391432
all_roots = []
392433

393434
for i, pi in enumerate(dCj.T):
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-
435+
r = _real_roots_in_01(pi)
409436
if len(r) > 0:
410437
all_roots.append(r)
411438
all_dims.append(np.full(len(r), i))

lib/matplotlib/tests/test_bezier.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,57 @@
22
Tests specific to the bezier module.
33
"""
44

5-
from matplotlib.bezier import inside_circle, split_bezier_intersecting_with_closedpath
5+
import pytest
6+
import numpy as np
7+
from numpy.testing import assert_allclose
8+
9+
from matplotlib.bezier import (
10+
_real_roots_in_01, inside_circle, split_bezier_intersecting_with_closedpath
11+
)
12+
13+
14+
def _np_real_roots_in_01(coeffs):
15+
"""Reference implementation using np.roots for comparison."""
16+
coeffs = np.asarray(coeffs)
17+
# np.roots expects descending order (highest power first)
18+
all_roots = np.roots(coeffs[::-1])
19+
# Filter to real roots in [0, 1]
20+
real_mask = np.abs(all_roots.imag) < 1e-10
21+
real_roots = all_roots[real_mask].real
22+
in_range = (real_roots >= -1e-10) & (real_roots <= 1 + 1e-10)
23+
return np.sort(np.clip(real_roots[in_range], 0, 1))
24+
25+
26+
@pytest.mark.parametrize("coeffs, expected", [
27+
([-0.5, 1], [0.5]),
28+
([-2, 1], []), # roots: [2.0], not in [0, 1]
29+
([0.1875, -1, 1], [0.25, 0.75]),
30+
([1, -2.5, 1], [0.5]), # roots: [0.5, 2.0], only one in [0, 1]
31+
([1, 0, 1], []), # roots: [+-i], not real
32+
([-0.08, 0.66, -1.5, 1], [0.2, 0.5, 0.8]),
33+
([5], []),
34+
([0, 0, 0], []),
35+
([0, -0.5, 1], [0.0, 0.5]),
36+
([0.5, -1.5, 1], [0.5, 1.0]),
37+
])
38+
def test_real_roots_in_01_known_cases(coeffs, expected):
39+
"""Test _real_roots_in_01 against known values and np.roots reference."""
40+
result = _real_roots_in_01(coeffs)
41+
np_expected = _np_real_roots_in_01(coeffs)
42+
assert_allclose(result, expected, atol=1e-10)
43+
assert_allclose(result, np_expected, atol=1e-10)
44+
45+
46+
@pytest.mark.parametrize("degree", range(1, 11))
47+
def test_real_roots_in_01_random(degree):
48+
"""Test random polynomials against np.roots."""
49+
rng = np.random.default_rng(seed=0)
50+
coeffs = rng.uniform(-10, 10, size=degree + 1)
51+
result = _real_roots_in_01(coeffs)
52+
expected = _np_real_roots_in_01(coeffs)
53+
assert len(result) == len(expected)
54+
if len(result) > 0:
55+
assert_allclose(result, expected, atol=1e-8)
656

757

858
def test_split_bezier_with_large_values():

0 commit comments

Comments
 (0)