Skip to content

Commit 5ef54b5

Browse files
committed
Merge PR #264: Defer optional SymPy imports until symbolic operations need them
2 parents f0c17a3 + 9c9dd30 commit 5ef54b5

6 files changed

Lines changed: 200 additions & 101 deletions

File tree

‎spatialmath/base/argcheck.py‎

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616
from collections.abc import Iterable
1717

1818
# from spatialmath.base import symbolic as sym # HACK
19-
from spatialmath.base.symbolic import issymbol, symtype
19+
from spatialmath.base.symbolic import issymbol
2020

2121
# valid scalar types
22-
_scalartypes = (int, np.integer, float, np.floating) + symtype
22+
_scalartypes = (int, np.integer, float, np.floating)
2323

2424
# from typing import Union, List, Tuple, Any, Optional, Type, Callable
2525
# from numpy.typing import DTypeLike
@@ -47,7 +47,9 @@ def isscalar(x: Any) -> bool:
4747
>>> isscalar([1])
4848
4949
"""
50-
return isinstance(x, _scalartypes)
50+
return isinstance(x, _scalartypes) or (
51+
not isinstance(x, (list, tuple)) and issymbol(x)
52+
)
5153

5254

5355
def isinteger(x: Any) -> bool:
@@ -386,7 +388,7 @@ def getvector(
386388
"""
387389
dt = dtype
388390

389-
if isinstance(v, _scalartypes): # handle scalar case
391+
if isscalar(v): # handle scalar case
390392
v = [v] # type: ignore
391393
if isinstance(v, (list, tuple)):
392394
# list or tuple was passed in
@@ -501,7 +503,7 @@ def isvector(v: Any, dim: Optional[int] = None) -> bool:
501503
if (
502504
isinstance(v, (list, tuple))
503505
and (dim is None or len(v) == dim)
504-
and all(map(lambda x: isinstance(x, _scalartypes), v))
506+
and all(map(isscalar, v))
505507
):
506508
return True # list or tuple
507509

@@ -516,7 +518,7 @@ def isvector(v: Any, dim: Optional[int] = None) -> bool:
516518
else:
517519
return s == (dim,) or s == (1, dim) or s == (dim, 1)
518520

519-
if (dim is None or dim == 1) and isinstance(v, _scalartypes):
521+
if (dim is None or dim == 1) and isscalar(v):
520522
return True
521523

522524
return False
@@ -620,11 +622,7 @@ def isnumberlist(x: Any) -> bool:
620622
>>> isnumberlist(np.r_[1,2])
621623
"""
622624

623-
return (
624-
isinstance(x, (list, tuple))
625-
and len(x) > 0
626-
and all(map(lambda x: isinstance(x, _scalartypes), x))
627-
)
625+
return isinstance(x, (list, tuple)) and len(x) > 0 and all(map(isscalar, x))
628626

629627

630628
def isvectorlist(x: Any, n: int) -> bool:

‎spatialmath/base/symbolic.py‎

Lines changed: 63 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -11,61 +11,67 @@
1111
supported.
1212
"""
1313

14+
from __future__ import annotations
15+
1416
import math
17+
from typing import TYPE_CHECKING
1518
from spatialmath.base.types import *
1619

17-
try: # pragma: no cover
18-
# print('Using SymPy')
19-
import sympy
20+
import importlib as _importlib
21+
import sys as _sys
2022

21-
_symbolics = True
22-
symtype = (sympy.Expr,)
23+
if TYPE_CHECKING:
2324
from sympy import Symbol
2425

25-
except ImportError: # pragma: no cover
26-
# SymPy is not installed
27-
_symbolics = False
28-
symtype = ()
29-
Symbol = Any
3026

27+
def _sympy():
28+
"""Load the optional dependency only for a symbolic operation."""
29+
return _importlib.import_module("sympy")
3130

32-
# ---------------------------------------------------------------------------------------#
3331

34-
if _symbolics:
32+
def __getattr__(name):
33+
"""Keep explicit access to the SymPy type aliases lazy."""
34+
if name == "Symbol":
35+
return _sympy().Symbol
36+
if name == "symtype":
37+
try:
38+
return (_sympy().Expr,)
39+
except ImportError:
40+
return ()
41+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
3542

36-
def symbol(
37-
name: str, real: Optional[bool] = True
38-
) -> Union[Symbol, Tuple[Symbol, ...]]:
39-
"""
40-
Create symbolic variables
4143

42-
:param name: symbol names
43-
:type name: str
44-
:param real: assume variable is real, defaults to True
45-
:type real: bool, optional
46-
:return: SymPy symbols
47-
:rtype: sympy
44+
def symbol(name: str, real: Optional[bool] = True) -> Union[Symbol, Tuple[Symbol, ...]]:
45+
"""
46+
Create symbolic variables
4847
49-
.. runblock:: pycon
48+
:param name: symbol names
49+
:type name: str
50+
:param real: assume variable is real, defaults to True
51+
:type real: bool, optional
52+
:return: SymPy symbols
53+
:rtype: sympy
5054
51-
>>> from spatialmath.base.symbolic import *
52-
>>> theta = symbol('theta')
53-
>>> theta
54-
>>> theta, psi = symbol('theta psi')
55-
>>> theta
56-
>>> psi
57-
>>> q = symbol('q_:6')
58-
>>> q
55+
.. runblock:: pycon
5956
60-
.. note:: In Jupyter symbols are pretty printed.
57+
>>> from spatialmath.base.symbolic import *
58+
>>> theta = symbol('theta')
59+
>>> theta
60+
>>> theta, psi = symbol('theta psi')
61+
>>> theta
62+
>>> psi
63+
>>> q = symbol('q_:6')
64+
>>> q
6165
62-
- symbols named after greek letters will appear as greek letters
63-
- underscore means subscript as it does in LaTex, so the symbols ``q``
64-
above will be subscripted.
66+
.. note:: In Jupyter symbols are pretty printed.
6567
66-
:seealso: :func:`sympy.symbols`
67-
"""
68-
return sympy.symbols(name, real=real)
68+
- symbols named after greek letters will appear as greek letters
69+
- underscore means subscript as it does in LaTex, so the symbols ``q``
70+
above will be subscripted.
71+
72+
:seealso: :func:`sympy.symbols`
73+
"""
74+
return _sympy().symbols(name, real=real)
6975

7076

7177
def issymbol(var: Any) -> bool:
@@ -84,13 +90,12 @@ def issymbol(var: Any) -> bool:
8490
>>> issymbol(3.4)
8591
8692
"""
87-
if _symbolics:
88-
if isinstance(var, (list, tuple)):
89-
return any([isinstance(x, symtype) for x in var])
90-
else:
91-
return isinstance(var, symtype)
92-
else:
93+
sympy = _sys.modules.get("sympy")
94+
if sympy is None:
9395
return False
96+
if isinstance(var, (list, tuple)):
97+
return any(isinstance(x, sympy.Expr) for x in var)
98+
return isinstance(var, sympy.Expr)
9499

95100

96101
@overload
@@ -122,7 +127,7 @@ def sin(theta):
122127
:seealso: :func:`sympy.sin`
123128
"""
124129
if issymbol(theta):
125-
return sympy.sin(theta)
130+
return _sympy().sin(theta)
126131
else:
127132
return math.sin(theta)
128133

@@ -156,7 +161,7 @@ def cos(theta):
156161
:seealso: :func:`sympy.cos`
157162
"""
158163
if issymbol(theta):
159-
return sympy.cos(theta)
164+
return _sympy().cos(theta)
160165
else:
161166
return math.cos(theta)
162167

@@ -190,7 +195,7 @@ def tan(theta):
190195
:seealso: :func:`sympy.cos`
191196
"""
192197
if issymbol(theta):
193-
return sympy.tan(theta)
198+
return _sympy().tan(theta)
194199
else:
195200
return math.tan(theta)
196201

@@ -224,7 +229,7 @@ def sqrt(v):
224229
:seealso: :func:`sympy.sqrt`
225230
"""
226231
if issymbol(v):
227-
return sympy.sqrt(v)
232+
return _sympy().sqrt(v)
228233
else:
229234
return math.sqrt(v)
230235

@@ -245,7 +250,7 @@ def zero() -> Symbol:
245250
246251
:seealso: :func:`sympy.S.Zero`
247252
"""
248-
return sympy.S.Zero
253+
return _sympy().S.Zero
249254

250255

251256
def one() -> Symbol:
@@ -264,7 +269,7 @@ def one() -> Symbol:
264269
265270
:seealso: :func:`sympy.S.One`
266271
"""
267-
return sympy.S.One
272+
return _sympy().S.One
268273

269274

270275
def negative_one() -> Symbol:
@@ -283,7 +288,7 @@ def negative_one() -> Symbol:
283288
284289
:seealso: :func:`sympy.S.NegativeOne`
285290
"""
286-
return sympy.S.NegativeOne
291+
return _sympy().S.NegativeOne
287292

288293

289294
def pi() -> Symbol:
@@ -302,7 +307,7 @@ def pi() -> Symbol:
302307
303308
:seealso: :func:`sympy.S.Pi`
304309
"""
305-
return sympy.S.Pi
310+
return _sympy().S.Pi
306311

307312

308313
def simplify(x: Symbol) -> Symbol:
@@ -324,10 +329,11 @@ def simplify(x: Symbol) -> Symbol:
324329
325330
:seealso: :func:`sympy.simplify`
326331
"""
327-
if _symbolics:
328-
return sympy.simplify(x)
329-
else:
332+
try:
333+
sympy = _sympy()
334+
except ImportError:
330335
return x
336+
return sympy.simplify(x)
331337

332338

333339
def det(x):
@@ -352,4 +358,4 @@ def det(x):
352358
.. note:: Converts to a SymPy ``Matrix`` and then back again.
353359
"""
354360

355-
return sympy.Matrix(x).det()
361+
return _sympy().Matrix(x).det()

‎spatialmath/base/transforms2d.py‎

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,6 @@
3939

4040
_eps = np.finfo(np.float64).eps
4141

42-
try: # pragma: no cover
43-
# print('Using SymPy')
44-
import sympy
45-
46-
_symbolics = True
47-
48-
except ImportError: # pragma: no cover
49-
_symbolics = False
50-
5142

5243
# ---------------------------------------------------------------------------------------#
5344
def rot2(theta: float, unit: str = "rad") -> SO2Array:
@@ -170,8 +161,13 @@ def tr2xyt(T: SE2Array, unit: str = "rad") -> R3:
170161
:seealso: trot2
171162
"""
172163

173-
if T.dtype == "O" and _symbolics:
174-
angle = sympy.atan2(T[1, 0], T[0, 0])
164+
if T.dtype == "O":
165+
try:
166+
import sympy
167+
except ImportError:
168+
angle = math.atan2(T[1, 0], T[0, 0])
169+
else:
170+
angle = sympy.atan2(T[1, 0], T[0, 0])
175171
else:
176172
angle = math.atan2(T[1, 0], T[0, 0])
177173
return np.r_[T[0, 2], T[1, 2], angle]

‎spatialmath/base/transformsNd.py‎

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,7 @@
2222
# from spatialmath.base.transforms2d import transl2
2323

2424

25-
try: # pragma: no cover
26-
# print('Using SymPy')
27-
from sympy import Matrix
28-
29-
_symbolics = True
30-
31-
except ImportError: # pragma: no cover
32-
_symbolics = False
25+
from spatialmath.base import symbolic as sym
3326

3427
_eps = np.finfo(np.float64).eps
3528

@@ -923,10 +916,12 @@ def det(m: np.ndarray) -> float:
923916
924917
:SymPy: supported
925918
"""
926-
if m.dtype.kind == "O" and _symbolics:
927-
return Matrix(m).det()
928-
else:
929-
return np.linalg.det(m)
919+
if m.dtype.kind == "O":
920+
try:
921+
return sym.det(m)
922+
except ImportError:
923+
pass
924+
return np.linalg.det(m)
930925

931926

932927
if __name__ == "__main__": # pragma: no cover

‎spatialmath/base/vectors.py‎

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,7 @@
1616
from spatialmath.base.argcheck import getvector
1717
from spatialmath.base.types import *
1818

19-
try: # pragma: no cover
20-
# print('Using SymPy')
21-
import sympy
22-
23-
_symbolics = True
24-
25-
except ImportError: # pragma: no cover
26-
_symbolics = False
19+
from spatialmath.base import symbolic as sym
2720

2821
_eps = np.finfo(np.float64).eps
2922

@@ -55,8 +48,8 @@ def norm(v: ArrayLikePure) -> float:
5548
for x in v:
5649
sum += x * x
5750

58-
if _symbolics and isinstance(sum, sympy.Expr):
59-
return sympy.sqrt(sum)
51+
if sym.issymbol(sum):
52+
return sym.sqrt(sum)
6053
else:
6154
return math.sqrt(sum)
6255

0 commit comments

Comments
 (0)