Skip to content

Commit

Permalink
Delay import of sympy until actually needed.
Browse files Browse the repository at this point in the history
sympy is a rather slow import (~330ms for me) which contributes nearly
half of bezier's import time (~750ms).  Delay the import until actually
needed.

As a typical use case where this is visible: I have a command-line
utility which does some computation using bezier.  Right now invoking
something as simple as `my-program --help` takes over a second to
complete (certainly a noticeable delay), in particular due to such slow
imports -- even if I do not use the symbolic computation part at all.

Obviously there are other ways in which this can be implemented, e.g. by
duplicating the `try... except ImportError` in each function, or with
another helper function, but given that you already have a
requires_sympy decorator, I thought I may as well reuse it for this
purpose.
  • Loading branch information
anntzer committed Jan 27, 2020
1 parent 8beb036 commit 24f30cf
Show file tree
Hide file tree
Showing 2 changed files with 21 additions and 32 deletions.
47 changes: 18 additions & 29 deletions src/python/bezier/_symbolic.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,34 +22,24 @@
:trim:
"""

import functools

try:
import sympy
except ImportError: # pragma: NO COVER
sympy = None


def require_sympy(wrapped):
"""Function decorator to require ``sympy`` to exist.
Args:
wrapped (Callable): A function to be wrapped.
def require_sympy():
"""Import and return ``sympy`` if it is installed, otherwise raise OSError.
Returns:
Callable: The wrapped function.
"""
module: The SymPy module
@functools.wraps(wrapped)
def ensure_sympy(*args, **kwargs):
if sympy is None:
raise OSError("This function requires SymPy.")
return wrapped(*args, **kwargs)
Raises
OSError: If SymPy is not installed.
"""

return ensure_sympy
try:
import sympy # pylint: disable=import-outside-toplevel
except ImportError: # pragma: NO COVER
raise OSError("This function requires SymPy.")
return sympy


@require_sympy
def to_symbolic(nodes):
"""Convert a 2D NumPy array to a SymPy matrix of rational numbers.
Expand All @@ -62,6 +52,7 @@ def to_symbolic(nodes):
Raises:
ValueError: If ``nodes`` is not 2D.
"""
sympy = require_sympy()
if nodes.ndim != 2:
raise ValueError("Nodes must be 2-dimensional, not", nodes.ndim)

Expand All @@ -70,7 +61,6 @@ def to_symbolic(nodes):
)


@require_sympy
def curve_weights(degree, s):
"""Compute de Casteljau weights for a curve.
Expand All @@ -87,6 +77,7 @@ def curve_weights(degree, s):
sympy.Matrix: The de Casteljau weights for the curve as a
``(degree + 1) x 1`` matrix.
"""
sympy = require_sympy()
return sympy.Matrix(
[
[sympy.binomial(degree, k) * s ** k * (1 - s) ** (degree - k)]
Expand All @@ -95,7 +86,6 @@ def curve_weights(degree, s):
)


@require_sympy
def curve_as_polynomial(nodes, degree):
"""Convert ``nodes`` into a SymPy polynomial array :math:`B(s)`.
Expand All @@ -109,6 +99,7 @@ def curve_as_polynomial(nodes, degree):
* The symbol ``s`` used in the polynomial
* The curve :math:`B(s)`.
"""
sympy = require_sympy()
nodes_sym = to_symbolic(nodes)

s = sympy.Symbol("s")
Expand All @@ -119,7 +110,6 @@ def curve_as_polynomial(nodes, degree):
return s, sympy.Matrix(factored).reshape(*b_polynomial.shape)


@require_sympy
def implicitize_2d(x_fn, y_fn, s):
"""Implicitize a 2D parametric curve.
Expand All @@ -132,11 +122,11 @@ def implicitize_2d(x_fn, y_fn, s):
sympy.Expr: The implicitized function :math:`f(x, y)` such that the
curve satisfies :math:`f(x(s), y(s)) = 0`.
"""
sympy = require_sympy()
x_sym, y_sym = sympy.symbols("x, y")
return sympy.resultant(x_fn - x_sym, y_fn - y_sym, s).factor()


@require_sympy
def implicitize_curve(nodes, degree):
"""Implicitize a 2D parametric curve, given the nodes.
Expand All @@ -160,7 +150,6 @@ def implicitize_curve(nodes, degree):
return implicitize_2d(x_fn, y_fn, s)


@require_sympy
def triangle_weights(degree, s, t):
"""Compute de Casteljau weights for a triangle.
Expand All @@ -178,6 +167,7 @@ def triangle_weights(degree, s, t):
sympy.Matrix: The de Casteljau weights for the triangle as an ``N x 1``
matrix, where ``N == (degree + 1)(degree + 2) / 2``.
"""
sympy = require_sympy()
lambda1 = 1 - s - t
lambda2 = s
lambda3 = t
Expand All @@ -194,7 +184,6 @@ def triangle_weights(degree, s, t):
return sympy.Matrix(values).reshape(len(values), 1)


@require_sympy
def triangle_as_polynomial(nodes, degree):
"""Convert ``nodes`` into a SymPy polynomial array :math:`B(s, t)`.
Expand All @@ -209,6 +198,7 @@ def triangle_as_polynomial(nodes, degree):
* The symbol ``t`` used in the polynomial
* The triangle :math:`B(s, t)`.
"""
sympy = require_sympy()
nodes_sym = to_symbolic(nodes)

s, t = sympy.symbols("s, t")
Expand All @@ -219,7 +209,6 @@ def triangle_as_polynomial(nodes, degree):
return s, t, sympy.Matrix(factored).reshape(*b_polynomial.shape)


@require_sympy
def implicitize_3d(x_fn, y_fn, z_fn, s, t):
"""Implicitize a 3D parametric triangle.
Expand All @@ -236,14 +225,14 @@ def implicitize_3d(x_fn, y_fn, z_fn, s, t):
sympy.Expr: The implicitized function :math:`f(x, y, z)` such that the
triangle satisfies :math:`f(x(s, t), y(s, t), z(s, t)) = 0`.
"""
sympy = require_sympy()
x_sym, y_sym, z_sym = sympy.symbols("x, y, z")

f_xy = sympy.resultant(x_fn - x_sym, y_fn - y_sym, s)
f_yz = sympy.resultant(y_fn - x_sym, z_fn - z_sym, s)
return sympy.resultant(f_xy, f_yz, t).factor()


@require_sympy
def implicitize_triangle(nodes, degree):
"""Implicitize a 3D parametric triangle, given the nodes.
Expand Down
6 changes: 3 additions & 3 deletions tests/unit/test__symbolic.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
# they were written before ``pytest`` was used in this project (the
# original test runner was ``nose``).

import sys

import numpy as np
import pytest

Expand Down Expand Up @@ -45,9 +47,7 @@ def _call_function_under_test(nodes):
return _symbolic.to_symbolic(nodes)

def test_sympy_missing(self, monkeypatch):
from bezier import _symbolic

monkeypatch.setattr(_symbolic, "sympy", None)
monkeypatch.setitem(sys.modules, "sympy", None)
with pytest.raises(OSError) as exc_info:
self._call_function_under_test(None)

Expand Down

0 comments on commit 24f30cf

Please sign in to comment.