pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/matplotlib/matplotlib/pull/31005/files

ref="https://github.githubassets.com/assets/code-34e10031edc15af1.css" /> PERF: Bezier root finding speedup by scottshambaugh · Pull Request #31005 · matplotlib/matplotlib · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 90 additions & 10 deletions lib/matplotlib/bezier.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,82 @@
from matplotlib import _api


def _quadratic_roots_in_01(c0, c1, c2):
"""Real roots of c0 + c1*x + c2*x**2 in [0, 1]."""
if abs(c2) < 1e-12: # Linear
if abs(c1) < 1e-12:
return np.array([])
root = -c0 / c1
return np.array([root]) if 0 <= root <= 1 else np.array([])

disc = c1 * c1 - 4 * c2 * c0
if disc < 0:
return np.array([])

sqrt_disc = np.sqrt(disc)
# Numerically stable quadratic formula
if c1 >= 0:
q = -0.5 * (c1 + sqrt_disc)
else:
q = -0.5 * (c1 - sqrt_disc)

roots = []
if abs(q) > 1e-12:
roots.append(c0 / q)
if abs(c2) > 1e-12:
roots.append(q / c2)

roots = np.asarray(roots)
return roots[(roots >= 0) & (roots <= 1)]


def _real_roots_in_01(coeffs):
"""
Find real roots of a polynomial in the interval [0, 1].

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
----------
coeffs : array-like
Polynomial coefficients in ascending order:
``c[0] + c[1]*x + c[2]*x**2 + ...``
Note this is the opposite convention from `numpy.roots`.

Returns
-------
roots : ndarray
Sorted array of real roots in [0, 1].
"""
coeffs = np.asarray(coeffs, dtype=float)

# Trim trailing near-zeros to get actual degree
deg = len(coeffs) - 1
while deg > 0 and abs(coeffs[deg]) < 1e-12:
deg -= 1

if deg <= 0:
return np.array([])
elif deg == 1:
root = -coeffs[0] / coeffs[1]
return np.array([root]) if 0 <= root <= 1 else np.array([])
elif deg == 2:
roots = _quadratic_roots_in_01(coeffs[0], coeffs[1], coeffs[2])
else:
# 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)


@lru_cache(maxsize=16)
def _get_coeff_matrix(n):
"""
Expand Down Expand Up @@ -309,17 +385,21 @@ def axis_aligned_extrema(self):
if n <= 1:
return np.array([]), np.array([])
Cj = self.polynomial_coefficients
dCj = np.arange(1, n+1)[:, None] * Cj[1:]
dims = []
roots = []
dCj = np.arange(1, n + 1)[:, None] * Cj[1:]

all_dims = []
all_roots = []
Comment thread
timhoffm marked this conversation as resolved.

for i, pi in enumerate(dCj.T):
r = np.roots(pi[::-1])
roots.append(r)
dims.append(np.full_like(r, i))
roots = np.concatenate(roots)
dims = np.concatenate(dims)
in_range = np.isreal(roots) & (roots >= 0) & (roots <= 1)
return dims[in_range], np.real(roots)[in_range]
r = _real_roots_in_01(pi)
if len(r) > 0:
all_roots.append(r)
all_dims.append(np.full(len(r), i))

if not all_roots:
return np.array([]), np.array([])

return np.concatenate(all_dims), np.concatenate(all_roots)


def split_bezier_intersecting_with_closedpath(
Expand Down
1 change: 1 addition & 0 deletions lib/matplotlib/bezier.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
33 changes: 32 additions & 1 deletion lib/matplotlib/tests/test_bezier.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,38 @@
Tests specific to the bezier module.
"""

from matplotlib.bezier import inside_circle, split_bezier_intersecting_with_closedpath
import pytest
import numpy as np
from numpy.testing import assert_allclose

from matplotlib.bezier import (
_real_roots_in_01, inside_circle, split_bezier_intersecting_with_closedpath
)


@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(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("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():
Expand Down
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





Check this box to remove all script contents from the fetched content.



Check this box to remove all images from the fetched content.


Check this box to remove all CSS styles from the fetched content.


Check this box to keep images inefficiently compressed and original size.

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