diff --git a/.readthedocs.yaml b/.readthedocs.yaml index b9f08311..e1df750e 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -23,3 +23,4 @@ python: extra_requirements: - dxf - rhino + - numba diff --git a/docs/installation.rst b/docs/installation.rst index f7014d2c..279d02a8 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -26,6 +26,18 @@ package index: pip install sectionproperties +Installing ``Numba`` +-------------------- + +``Numba`` translates a subset of Python and NumPy code into fast machine code, allowing +algorithms to approach the speeds of C. The speed of several ``sectionproperties`` +analysis functions have been enhanced with `numba `_. +To take advantage of this increase in performance you can install ``numba`` alongside +``sectionproperties`` with: + +.. code-block:: shell + + pip install sectionproperties[numba] Installing ``PARDISO`` Solver ----------------------------- diff --git a/noxfile.py b/noxfile.py index 2fcfcc4b..0756c904 100644 --- a/noxfile.py +++ b/noxfile.py @@ -212,7 +212,13 @@ def docs_build(session: Session) -> None: args.insert(0, "--color") session.run_always( - "poetry", "install", "--only", "main", "--extras", "dxf rhino", external=True + "poetry", + "install", + "--only", + "main", + "--extras", + "dxf rhino numba", + external=True, ) session.install( "furo", @@ -243,7 +249,13 @@ def docs(session: Session) -> None: """ args = session.posargs or ["--open-browser", "docs", "docs/_build"] session.run_always( - "poetry", "install", "--only", "main", "--extras", "dxf rhino", external=True + "poetry", + "install", + "--only", + "main", + "--extras", + "dxf rhino numba", + external=True, ) session.install( "furo", diff --git a/poetry.lock b/poetry.lock index 905281a9..fb804fa6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1755,7 +1755,7 @@ tornado = {version = "*", markers = "python_version > \"2.7\""} name = "llvmlite" version = "0.41.0" description = "lightweight wrapper around basic LLVM functionality" -optional = false +optional = true python-versions = ">=3.8" files = [ {file = "llvmlite-0.41.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:acc81c1279f858e5eab460844cc381e30d6666bc8eea04724b54d4eeb1fd1e54"}, @@ -2219,7 +2219,7 @@ test = ["pytest", "pytest-console-scripts", "pytest-jupyter", "pytest-tornasync" name = "numba" version = "0.58.0" description = "compiling Python code using LLVM" -optional = false +optional = true python-versions = ">=3.8" files = [ {file = "numba-0.58.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f146c11af62ad25021d93fccf48715a96d1ea76d43c1c3bc97dca561c6a2693"}, @@ -4068,10 +4068,11 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [extras] dxf = ["cad-to-shapely"] +numba = ["numba"] pardiso = ["pypardiso"] rhino = ["rhino-shapley-interop", "rhino3dm"] [metadata] lock-version = "2.0" python-versions = ">=3.9.0,<3.12" -content-hash = "ec8bdd1ec8cf8b11014cd9f46f4fd5e33f5d90293edfc82c8ba6901a5c07a6a4" +content-hash = "fc7856a00a6a1e5b6be85d69b75d593ceef9e498796bb546dee0b93e90afbaf4" diff --git a/pyproject.toml b/pyproject.toml index 19586ab0..2bd7661f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ Changelog = "https://github.com/robbievanleeuwen/section-properties/releases" [tool.poetry.dependencies] python = ">=3.9.0,<3.12" -numpy = "^1.25.2" # numba requires numpy <1.26 +numpy = "^1.25.2" scipy = "^1.11.3" matplotlib = "^3.8.0" shapely = "^2.0.1" @@ -56,7 +56,7 @@ triangle = "^20230923" rich = "^13.6.0" click = "^8.1.7" more-itertools = "^10.1.0" -numba = "^0.58.0" +numba = { version = "^0.58.0", optional = true } cad-to-shapely = { version = "^0.3.1", optional = true } rhino-shapley-interop = { version = "^0.0.4", optional = true } rhino3dm = { version = "==8.0.0b3", optional = true } @@ -97,6 +97,7 @@ sphinxext-opengraph = "^0.8.2" [tool.poetry.extras] dxf = ["cad-to-shapely"] rhino = ["rhino-shapley-interop", "rhino3dm"] +numba = ["numba"] pardiso = ["pypardiso"] [tool.poetry.scripts] diff --git a/src/sectionproperties/analysis/fea.py b/src/sectionproperties/analysis/fea.py index 2626d38d..e6ab5df7 100644 --- a/src/sectionproperties/analysis/fea.py +++ b/src/sectionproperties/analysis/fea.py @@ -10,19 +10,67 @@ import warnings from dataclasses import dataclass, field from functools import lru_cache -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Callable import numpy as np import numpy.typing as npt -from numba import njit -from numba.core.errors import NumbaPerformanceWarning if TYPE_CHECKING: from sectionproperties.pre.pre import Material -@njit(cache=True, nogil=True) # type: ignore +# numba is an optional dependency +try: + from numba import njit + from numba.core.errors import NumbaPerformanceWarning + + USE_NUMBA = True +except ImportError: + + def njit() -> None: + """Assigns empty function to njit if numba isn't installed. + + Returns: + None + """ + return None + + USE_NUMBA = False + + +def conditional_decorator( + dec: Callable[[Any], Any], + condition: bool, +) -> Callable[[Any], Any]: + """A decorator that applies a decorator only if a condition is True. + + Args: + dec: Decorator to apply + condition: Apply decorator if this is true + + Returns: + Decorator wrapper + """ + + def decorator(func: Callable[[Any], Any]) -> Callable[[Any], Any]: + """Decorator wrapper. + + Args: + func: Function decorator operates on. + + Returns: + Original or decorated function. + """ + if not condition: + return func + + return dec(func) # type: ignore + + return decorator + + +@conditional_decorator(njit, USE_NUMBA) def _assemble_torsion( k_el: npt.NDArray[np.float64], f_el: npt.NDArray[np.float64], @@ -55,7 +103,7 @@ def _assemble_torsion( return k_el, f_el, c_el -@njit(cache=True, nogil=True) # type: ignore +@conditional_decorator(njit, USE_NUMBA) def _shear_parameter( nx: float, ny: float, ixx: float, iyy: float, ixy: float ) -> tuple[float, float, float, float, float, float]: @@ -81,7 +129,7 @@ def _shear_parameter( return r, q, d1, d2, h1, h2 -@njit(cache=True, nogil=True) # type: ignore +@conditional_decorator(njit, USE_NUMBA) def _assemble_shear_load( f_psi: npt.NDArray[np.float64], f_phi: npt.NDArray[np.float64], @@ -126,7 +174,7 @@ def _assemble_shear_load( return f_psi, f_phi -@njit(cache=True, nogil=True) # type: ignore +@conditional_decorator(njit, USE_NUMBA) def _assemble_shear_coefficients( kappa_x: float, kappa_y: float, @@ -648,7 +696,9 @@ def element_stress( # extrapolate results to nodes, ignore numba warnings about performance with warnings.catch_warnings(): - warnings.simplefilter("ignore", category=NumbaPerformanceWarning) + if USE_NUMBA: + warnings.simplefilter("ignore", category=NumbaPerformanceWarning) + sig_zz_mxx = extrapolate_to_nodes(w=sig_zz_mxx_gp) sig_zz_myy = extrapolate_to_nodes(w=sig_zz_myy_gp) sig_zz_m11 = extrapolate_to_nodes(w=sig_zz_m11_gp) @@ -951,7 +1001,7 @@ def gauss_points(*, n: int) -> npt.NDArray[np.float64]: @lru_cache(maxsize=None) -@njit(cache=True, nogil=True) # type: ignore +@conditional_decorator(njit, USE_NUMBA) def __shape_function_cached( coords: tuple[float, ...], gauss_point: tuple[float, float, float], @@ -1039,7 +1089,7 @@ def shape_function( @lru_cache(maxsize=None) -@njit(cache=True, nogil=True) # type: ignore +@conditional_decorator(njit, USE_NUMBA) def shape_function_only(p: tuple[float, float, float]) -> npt.NDArray[np.float64]: """The values of the ``Tri6`` shape function at a point ``p``. @@ -1117,7 +1167,7 @@ def shape_function_only(p: tuple[float, float, float]) -> npt.NDArray[np.float64 ) -@njit(cache=True, nogil=True) # type: ignore +@conditional_decorator(njit, USE_NUMBA) def extrapolate_to_nodes(w: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]: """Extrapolates results at six Gauss points to the six nodes of a ``Tri6`` element. @@ -1130,7 +1180,7 @@ def extrapolate_to_nodes(w: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]: return h_inv @ w -@njit(cache=True, nogil=True) # type: ignore +@conditional_decorator(njit, USE_NUMBA) def principal_coordinate( phi: float, x: float, @@ -1153,7 +1203,7 @@ def principal_coordinate( return x * cos_phi + y * sin_phi, y * cos_phi - x * sin_phi -@njit(cache=True, nogil=True) # type: ignore +@conditional_decorator(njit, USE_NUMBA) def global_coordinate( phi: float, x11: float, @@ -1176,7 +1226,7 @@ def global_coordinate( return x11 * cos_phi - y22 * sin_phi, x11 * sin_phi + y22 * cos_phi -@njit(cache=True, nogil=True) # type: ignore +@conditional_decorator(njit, USE_NUMBA) def point_above_line( u: npt.NDArray[np.float64], px: float,