--- a PPN by Garber Painting Akron. With Image Size Reduction included!URL: http://github.com/matplotlib/matplotlib/pull/31005.patch
ials against np.roots."""
+ rng = np.random.default_rng(seed=0)
+ coeffs = rng.uniform(-10, 10, size=degree + 1)
+ result = _real_roots_in_01(coeffs)
+ expected = _np_real_roots_in_01(coeffs)
+ assert len(result) == len(expected)
+ if len(result) > 0:
+ assert_allclose(result, expected, atol=1e-8)
def test_split_bezier_with_large_values():
From 52cd7dde7a5eb0cc29addefc091bc3c3a6fab40f Mon Sep 17 00:00:00 2001
From: Scott Shambaugh
Date: Tue, 3 Feb 2026 16:54:55 -0700
Subject: [PATCH 3/5] Fix stub
---
lib/matplotlib/bezier.pyi | 1 +
1 file changed, 1 insertion(+)
diff --git a/lib/matplotlib/bezier.pyi b/lib/matplotlib/bezier.pyi
index ad82b873affd..d50328bba8a3 100644
--- a/lib/matplotlib/bezier.pyi
+++ b/lib/matplotlib/bezier.pyi
@@ -22,6 +22,7 @@ def get_normal_points(
cx: float, cy: float, cos_t: float, sin_t: float, length: float
) -> tuple[float, float, float, float]: ...
def split_de_casteljau(beta: ArrayLike, t: float) -> tuple[np.ndarray, np.ndarray]: ...
+def _real_roots_in_01(coeffs: ArrayLike) -> np.ndarray: ...
def find_bezier_t_intersecting_with_closedpath(
bezier_point_at_t: Callable[[float], tuple[float, float]],
inside_closedpath: Callable[[tuple[float, float]], bool],
From d007209f136e1ef552356a7f4707f517d85004f4 Mon Sep 17 00:00:00 2001
From: Scott Shambaugh
Date: Thu, 5 Mar 2026 20:31:47 -0700
Subject: [PATCH 4/5] Simplify bezier root finding
---
lib/matplotlib/bezier.py | 63 +++++++---------------------------------
1 file changed, 11 insertions(+), 52 deletions(-)
diff --git a/lib/matplotlib/bezier.py b/lib/matplotlib/bezier.py
index 3dc30fe5c6a4..e2ffe7747e55 100644
--- a/lib/matplotlib/bezier.py
+++ b/lib/matplotlib/bezier.py
@@ -9,53 +9,6 @@
import numpy as np
from matplotlib import _api
-from numpy.polynomial.polynomial import polyval as _polyval
-
-
-def _bisect_root_finder(f, a, b, tol=1e-12, max_iter=64):
- """Find root of f in [a, b] using bisection. Assumes sign change exists."""
- fa = f(a)
- for _ in range(max_iter):
- mid = (a + b) * 0.5
- fm = f(mid)
- if abs(fm) < tol or (b - a) < tol:
- return mid
- if fa * fm < 0:
- b = mid
- else:
- a, fa = mid, fm
- return (a + b) * 0.5
-
-
-def _bisected_roots_in_01(coeffs):
- """
- Find real roots of polynomial in [0, 1] using sampling and bisection.
- coeffs in ascending order: c0 + c1*x + c2*x**2 + ...
- """
- deg = len(coeffs) - 1
- n_samples = max(8, deg * 2)
- ts = np.linspace(0, 1, n_samples)
- vals = _polyval(ts, coeffs)
-
- signs = np.sign(vals)
- sign_changes = np.where((signs[:-1] != signs[1:]) & (signs[:-1] != 0))[0]
-
- roots = []
-
- def f(t):
- return _polyval(t, coeffs)
-
- max_iter = 53 # float64 fractional precision for [0, 1] interval
- for i in sign_changes:
- roots.append(_bisect_root_finder(f, ts[i], ts[i + 1], max_iter=max_iter))
-
- # Check endpoints
- if abs(vals[0]) < 1e-12:
- roots.insert(0, 0.0)
- if abs(vals[-1]) < 1e-12 and (not roots or abs(roots[-1] - 1.0) > 1e-10):
- roots.append(1.0)
-
- return np.asarray(roots)
def _quadratic_roots_in_01(c0, c1, c2):
@@ -91,10 +44,10 @@ def _real_roots_in_01(coeffs):
"""
Find real roots of a polynomial in the interval [0, 1].
- This is optimized for finding roots only in [0, 1], which is faster than
- computing all roots with `numpy.roots` and filtering. For polynomials of
- degree <= 2, closed-form solutions are used. For higher degrees, sampling
- and bisection are used.
+ For polynomials of degree <= 2, closed-form solutions are used.
+ For higher degrees, `numpy.roots` is used as a fallback. In practice,
+ matplotlib only ever uses cubic bezier curves and axis_aligned_extrema()
+ differentiates, so we only ever find roots for degree <= 2.
Parameters
----------
@@ -123,7 +76,13 @@ def _real_roots_in_01(coeffs):
elif deg == 2:
roots = _quadratic_roots_in_01(coeffs[0], coeffs[1], coeffs[2])
else:
- roots = _bisected_roots_in_01(coeffs[:deg + 1])
+ # np.roots expects descending order (highest power first)
+ eps = 1e-10
+ all_roots = np.roots(coeffs[deg::-1])
+ real_mask = np.abs(all_roots.imag) < eps
+ real_roots = all_roots[real_mask].real
+ in_range = (real_roots >= -eps) & (real_roots <= 1 + eps)
+ roots = np.clip(real_roots[in_range], 0, 1)
return np.sort(roots)
From 9a256484618172acb93f573e4bb1389eefed7b71 Mon Sep 17 00:00:00 2001
From: Scott Shambaugh
Date: Fri, 6 Mar 2026 12:27:37 -0700
Subject: [PATCH 5/5] Update bezier tests
---
lib/matplotlib/tests/test_bezier.py | 59 ++++++++++-------------------
1 file changed, 20 insertions(+), 39 deletions(-)
diff --git a/lib/matplotlib/tests/test_bezier.py b/lib/matplotlib/tests/test_bezier.py
index 42ae468579ca..ad5e5acfe49e 100644
--- a/lib/matplotlib/tests/test_bezier.py
+++ b/lib/matplotlib/tests/test_bezier.py
@@ -11,48 +11,29 @@
)
-def _np_real_roots_in_01(coeffs):
- """Reference implementation using np.roots for comparison."""
- coeffs = np.asarray(coeffs)
- # np.roots expects descending order (highest power first)
- all_roots = np.roots(coeffs[::-1])
- # Filter to real roots in [0, 1]
- real_mask = np.abs(all_roots.imag) < 1e-10
- real_roots = all_roots[real_mask].real
- in_range = (real_roots >= -1e-10) & (real_roots <= 1 + 1e-10)
- return np.sort(np.clip(real_roots[in_range], 0, 1))
-
-
-@pytest.mark.parametrize("coeffs, expected", [
- ([-0.5, 1], [0.5]),
- ([-2, 1], []), # roots: [2.0], not in [0, 1]
- ([0.1875, -1, 1], [0.25, 0.75]),
- ([1, -2.5, 1], [0.5]), # roots: [0.5, 2.0], only one in [0, 1]
- ([1, 0, 1], []), # roots: [+-i], not real
- ([-0.08, 0.66, -1.5, 1], [0.2, 0.5, 0.8]),
- ([5], []),
- ([0, 0, 0], []),
- ([0, -0.5, 1], [0.0, 0.5]),
- ([0.5, -1.5, 1], [0.5, 1.0]),
+@pytest.mark.parametrize("roots, expected_in_01", [
+ ([0.5], [0.5]),
+ ([0.25, 0.75], [0.25, 0.75]),
+ ([0.2, 0.5, 0.8], [0.2, 0.5, 0.8]),
+ ([0.1, 0.2, 0.3, 0.4], [0.1, 0.2, 0.3, 0.4]),
+ ([0.0, 0.5], [0.0, 0.5]),
+ ([0.5, 1.0], [0.5, 1.0]),
+ ([2.0], []), # outside [0, 1]
+ ([0.5, 2.0], [0.5]), # one in, one out
+ ([-1j, 1j], []), # complex roots
+ ([0.5, -1j, 1j], [0.5]), # mix of real and complex
+ ([0.3, 0.3], [0.3, 0.3]), # repeated root
])
-def test_real_roots_in_01_known_cases(coeffs, expected):
- """Test _real_roots_in_01 against known values and np.roots reference."""
- result = _real_roots_in_01(coeffs)
- np_expected = _np_real_roots_in_01(coeffs)
- assert_allclose(result, expected, atol=1e-10)
- assert_allclose(result, np_expected, atol=1e-10)
+def test_real_roots_in_01(roots, expected_in_01):
+ roots = np.array(roots)
+ coeffs = np.poly(roots)[::-1] # np.poly gives descending, we need ascending
+ result = _real_roots_in_01(coeffs.real)
+ assert_allclose(result, expected_in_01, atol=1e-10)
-@pytest.mark.parametrize("degree", range(1, 11))
-def test_real_roots_in_01_random(degree):
- """Test random polynomials against np.roots."""
- rng = np.random.default_rng(seed=0)
- coeffs = rng.uniform(-10, 10, size=degree + 1)
- result = _real_roots_in_01(coeffs)
- expected = _np_real_roots_in_01(coeffs)
- assert len(result) == len(expected)
- if len(result) > 0:
- assert_allclose(result, expected, atol=1e-8)
+@pytest.mark.parametrize("coeffs", [[5], [0, 0, 0]])
+def test_real_roots_in_01_no_roots(coeffs):
+ assert len(_real_roots_in_01(coeffs)) == 0
def test_split_bezier_with_large_values():
pFad - Phonifier reborn
Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy