Skip to content

Commit 380c968

Browse files
authored
Defer scipy imports so import spatialmath is faster (#244)
Part of #234 (the SciPy half; SymPy is not changed here). `spatialmath/base/quaternions.py` and `spatialmath/spline.py` imported `scipy.interpolate` / `scipy.spatial.transform` at module level, so `import spatialmath` pulled in scipy even though only `qrand` (with a theta range), `SplineSE3` and `BSplineSE3` use it. These imports now happen inside those functions. The `interpolate.interp1d` return annotation stays as a string under `TYPE_CHECKING`. Measured with `python -X importtime -c "import spatialmath"` on one machine, single run each: 1.42 s before, 0.40 s after. `scipy` is no longer in `sys.modules` after `import spatialmath`. `sympy` still is, so the remaining part of #234 (`issymbol` / `argcheck`) is untouched. Added `tests/test_import.py`, which imports spatialmath in a subprocess and checks that no scipy module is loaded. It fails on master and passes here. Run: full `tests/` suite with `MPLBACKEND=Agg`, 348 passed, 3 skipped; black 23.10.0 clean on the changed files; `qrand(theta_range=...)` and importing `SplineSE3`/`BSplineSE3` smoke-checked. Not run: the spline classes were not exercised by hand beyond what the existing tests cover, and only Python 3 on Linux was tried.
1 parent d1a945d commit 380c968

3 files changed

Lines changed: 28 additions & 5 deletions

File tree

‎spatialmath/base/quaternions.py‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@
1616
import spatialmath.base as smb
1717
from spatialmath.base.argcheck import getunit
1818
from spatialmath.base.types import *
19-
import scipy.interpolate as interpolate
20-
from typing import Optional
19+
from typing import TYPE_CHECKING, Optional
2120
from functools import lru_cache
2221
import warnings
2322

23+
if TYPE_CHECKING:
24+
import scipy.interpolate as interpolate
25+
2426
_eps = np.finfo(np.float64).eps
2527

2628

@@ -920,7 +922,7 @@ def _compute_cdf_sin_squared(theta: float):
920922
@lru_cache(maxsize=1)
921923
def _generate_inv_cdf_sin_squared_interp(
922924
num_interpolation_points: int = 256,
923-
) -> interpolate.interp1d:
925+
) -> "interpolate.interp1d":
924926
"""
925927
Computes an interpolation function for the inverse CDF of the distribution of angular magnitude.
926928
@@ -933,6 +935,8 @@ def _generate_inv_cdf_sin_squared_interp(
933935
This function returns interpolation function for the inverse of the integral of the
934936
pdf of angular magnitudes (2/pi * sin^2(theta/2)), which is not analytically defined.
935937
"""
938+
import scipy.interpolate as interpolate # deferred, it is slow to import
939+
936940
cdf_sin_squared_interp_angles = np.linspace(0, np.pi, num_interpolation_points)
937941
cdf_sin_squared_interp_values = _compute_cdf_sin_squared(
938942
cdf_sin_squared_interp_angles

‎spatialmath/spline.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111
from typing import TYPE_CHECKING, List, Optional, Tuple
1212

1313
import numpy as np
14-
from scipy.interpolate import BSpline, CubicSpline
15-
from scipy.spatial.transform import Rotation, RotationSpline
1614

1715
from spatialmath import SE3, SO3, Twist3
1816
from spatialmath.base.transforms3d import tranimate
@@ -122,6 +120,9 @@ def __init__(
122120
self.timepoints = self.timepoints - self.timepoints[0]
123121
self.timepoints = self.timepoints / self.timepoints[-1]
124122

123+
from scipy.interpolate import CubicSpline # deferred, scipy is slow to import
124+
from scipy.spatial.transform import Rotation, RotationSpline
125+
125126
self.spline_xyz = CubicSpline(
126127
self.timepoints,
127128
np.array([pose.t for pose in self.control_poses]),
@@ -292,6 +293,8 @@ def __init__(
292293
) # ensures the curve ends on the last control pose
293294
self.knots = knots
294295

296+
from scipy.interpolate import BSpline # deferred, scipy is slow to import
297+
295298
self.splines = [
296299
BSpline(knots, self.control_pose_matrix[:, i], degree)
297300
for i in range(0, 6) # twists are length 6

‎tests/test_import.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import subprocess
2+
import sys
3+
import unittest
4+
5+
6+
class TestImport(unittest.TestCase):
7+
def test_import_does_not_load_scipy(self):
8+
# scipy is slow to import and only a few functions need it, so it is
9+
# imported by those functions and not when spatialmath is imported
10+
code = "import sys, spatialmath; sys.exit(any(m.split('.')[0] == 'scipy' for m in sys.modules))"
11+
result = subprocess.run([sys.executable, "-c", code])
12+
self.assertEqual(result.returncode, 0, "import spatialmath loaded scipy")
13+
14+
15+
if __name__ == "__main__": # pragma nocover
16+
unittest.main()

0 commit comments

Comments
 (0)