Skip to content
Open
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
20 changes: 9 additions & 11 deletions spatialmath/base/argcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
from collections.abc import Iterable

# from spatialmath.base import symbolic as sym # HACK
from spatialmath.base.symbolic import issymbol, symtype
from spatialmath.base.symbolic import issymbol

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

# from typing import Union, List, Tuple, Any, Optional, Type, Callable
# from numpy.typing import DTypeLike
Expand Down Expand Up @@ -47,7 +47,9 @@ def isscalar(x: Any) -> bool:
>>> isscalar([1])

"""
return isinstance(x, _scalartypes)
return isinstance(x, _scalartypes) or (
not isinstance(x, (list, tuple)) and issymbol(x)
)


def isinteger(x: Any) -> bool:
Expand Down Expand Up @@ -386,7 +388,7 @@ def getvector(
"""
dt = dtype

if isinstance(v, _scalartypes): # handle scalar case
if isscalar(v): # handle scalar case
v = [v] # type: ignore
if isinstance(v, (list, tuple)):
# list or tuple was passed in
Expand Down Expand Up @@ -501,7 +503,7 @@ def isvector(v: Any, dim: Optional[int] = None) -> bool:
if (
isinstance(v, (list, tuple))
and (dim is None or len(v) == dim)
and all(map(lambda x: isinstance(x, _scalartypes), v))
and all(map(isscalar, v))
):
return True # list or tuple

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

if (dim is None or dim == 1) and isinstance(v, _scalartypes):
if (dim is None or dim == 1) and isscalar(v):
return True

return False
Expand Down Expand Up @@ -620,11 +622,7 @@ def isnumberlist(x: Any) -> bool:
>>> isnumberlist(np.r_[1,2])
"""

return (
isinstance(x, (list, tuple))
and len(x) > 0
and all(map(lambda x: isinstance(x, _scalartypes), x))
)
return isinstance(x, (list, tuple)) and len(x) > 0 and all(map(isscalar, x))


def isvectorlist(x: Any, n: int) -> bool:
Expand Down
139 changes: 73 additions & 66 deletions spatialmath/base/symbolic.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,61 +11,67 @@
supported.
"""

from __future__ import annotations

import math
from typing import TYPE_CHECKING
from spatialmath.base.types import *

try: # pragma: no cover
# print('Using SymPy')
import sympy
import importlib as _importlib
import sys as _sys

_symbolics = True
symtype = (sympy.Expr,)
if TYPE_CHECKING:
from sympy import Symbol

except ImportError: # pragma: no cover
# SymPy is not installed
_symbolics = False
symtype = ()
Symbol = Any

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

# ---------------------------------------------------------------------------------------#

if _symbolics:
def __getattr__(name):
"""Keep explicit access to the SymPy type aliases lazy."""
if name == "Symbol":
return _sympy().Symbol
if name == "symtype":
try:
return (_sympy().Expr,)
except ImportError:
return ()
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

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

:param name: symbol names
:type name: str
:param real: assume variable is real, defaults to True
:type real: bool, optional
:return: SymPy symbols
:rtype: sympy
def symbol(name: str, real: Optional[bool] = True) -> Union[Symbol, Tuple[Symbol, ...]]:
"""
Create symbolic variables

.. runblock:: pycon
:param name: symbol names
:type name: str
:param real: assume variable is real, defaults to True
:type real: bool, optional
:return: SymPy symbols
:rtype: sympy

>>> from spatialmath.base.symbolic import *
>>> theta = symbol('theta')
>>> theta
>>> theta, psi = symbol('theta psi')
>>> theta
>>> psi
>>> q = symbol('q_:6')
>>> q
.. runblock:: pycon

>>> from spatialmath.base.symbolic import *
>>> theta = symbol('theta')
>>> theta
>>> theta, psi = symbol('theta psi')
>>> theta
>>> psi
>>> q = symbol('q_:6')
>>> q

.. note:: In Jupyter symbols are pretty printed.
.. note:: In Jupyter symbols are pretty printed.

- symbols named after greek letters will appear as greek letters
- underscore means subscript as it does in LaTex, so the symbols ``q``
above will be subscripted.
- symbols named after greek letters will appear as greek letters
- underscore means subscript as it does in LaTex, so the symbols ``q``
above will be subscripted.

:seealso: :func:`sympy.symbols`
"""
return sympy.symbols(name, real=real)
:seealso: :func:`sympy.symbols`
"""
return _sympy().symbols(name, real=real)


def issymbol(var: Any) -> bool:
Expand All @@ -84,13 +90,12 @@ def issymbol(var: Any) -> bool:
>>> issymbol(3.4)

"""
if _symbolics:
if isinstance(var, (list, tuple)):
return any([isinstance(x, symtype) for x in var])
else:
return isinstance(var, symtype)
else:
sympy = _sys.modules.get("sympy")
if sympy is None:
return False
if isinstance(var, (list, tuple)):
return any(isinstance(x, sympy.Expr) for x in var)
return isinstance(var, sympy.Expr)


@overload
Expand Down Expand Up @@ -122,7 +127,7 @@ def sin(theta):
:seealso: :func:`sympy.sin`
"""
if issymbol(theta):
return sympy.sin(theta)
return _sympy().sin(theta)
else:
return math.sin(theta)

Expand Down Expand Up @@ -156,7 +161,7 @@ def cos(theta):
:seealso: :func:`sympy.cos`
"""
if issymbol(theta):
return sympy.cos(theta)
return _sympy().cos(theta)
else:
return math.cos(theta)

Expand Down Expand Up @@ -190,7 +195,7 @@ def tan(theta):
:seealso: :func:`sympy.cos`
"""
if issymbol(theta):
return sympy.tan(theta)
return _sympy().tan(theta)
else:
return math.tan(theta)

Expand Down Expand Up @@ -224,7 +229,7 @@ def sqrt(v):
:seealso: :func:`sympy.sqrt`
"""
if issymbol(v):
return sympy.sqrt(v)
return _sympy().sqrt(v)
else:
return math.sqrt(v)

Expand All @@ -245,7 +250,7 @@ def zero() -> Symbol:

:seealso: :func:`sympy.S.Zero`
"""
return sympy.S.Zero
return _sympy().S.Zero


def one() -> Symbol:
Expand All @@ -264,7 +269,7 @@ def one() -> Symbol:

:seealso: :func:`sympy.S.One`
"""
return sympy.S.One
return _sympy().S.One


def negative_one() -> Symbol:
Expand All @@ -283,26 +288,27 @@ def negative_one() -> Symbol:

:seealso: :func:`sympy.S.NegativeOne`
"""
return sympy.S.NegativeOne
return _sympy().S.NegativeOne


def pi() -> Symbol:
"""
Symbolic constant: pi
Symbolic constant: pi

:return: π
:rtype: symbolic
:return: π
:rtype: symbolic

.. runblock:: pycon
.. runblock:: pycon

>>> from spatialmath.base.symbolic import *
>>> import math
>>> sin(pi())
>>> sin(math.pi)
>>> from spatialmath.base.symbolic import *
>>> import math
from typing import TYPE_CHECKING
>>> sin(pi())
>>> sin(math.pi)

:seealso: :func:`sympy.S.Pi`
:seealso: :func:`sympy.S.Pi`
"""
return sympy.S.Pi
return _sympy().S.Pi


def simplify(x: Symbol) -> Symbol:
Expand All @@ -324,10 +330,11 @@ def simplify(x: Symbol) -> Symbol:

:seealso: :func:`sympy.simplify`
"""
if _symbolics:
return sympy.simplify(x)
else:
try:
sympy = _sympy()
except ImportError:
return x
return sympy.simplify(x)


def det(x):
Expand All @@ -352,4 +359,4 @@ def det(x):
.. note:: Converts to a SymPy ``Matrix`` and then back again.
"""

return sympy.Matrix(x).det()
return _sympy().Matrix(x).det()
18 changes: 7 additions & 11 deletions spatialmath/base/transforms2d.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,6 @@

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

try: # pragma: no cover
# print('Using SymPy')
import sympy

_symbolics = True

except ImportError: # pragma: no cover
_symbolics = False


# ---------------------------------------------------------------------------------------#
def rot2(theta: float, unit: str = "rad") -> SO2Array:
Expand Down Expand Up @@ -170,8 +161,13 @@ def tr2xyt(T: SE2Array, unit: str = "rad") -> R3:
:seealso: trot2
"""

if T.dtype == "O" and _symbolics:
angle = sympy.atan2(T[1, 0], T[0, 0])
if T.dtype == "O":
try:
import sympy
except ImportError:
angle = math.atan2(T[1, 0], T[0, 0])
else:
angle = sympy.atan2(T[1, 0], T[0, 0])
else:
angle = math.atan2(T[1, 0], T[0, 0])
return np.r_[T[0, 2], T[1, 2], angle]
Expand Down
19 changes: 7 additions & 12 deletions spatialmath/base/transformsNd.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,7 @@
# from spatialmath.base.transforms2d import transl2


try: # pragma: no cover
# print('Using SymPy')
from sympy import Matrix

_symbolics = True

except ImportError: # pragma: no cover
_symbolics = False
from spatialmath.base import symbolic as sym

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

Expand Down Expand Up @@ -905,10 +898,12 @@ def det(m: np.ndarray) -> float:

:SymPy: supported
"""
if m.dtype.kind == "O" and _symbolics:
return Matrix(m).det()
else:
return np.linalg.det(m)
if m.dtype.kind == "O":
try:
return sym.det(m)
except ImportError:
pass
return np.linalg.det(m)


if __name__ == "__main__": # pragma: no cover
Expand Down
13 changes: 3 additions & 10 deletions spatialmath/base/vectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,7 @@
from spatialmath.base.argcheck import getvector
from spatialmath.base.types import *

try: # pragma: no cover
# print('Using SymPy')
import sympy

_symbolics = True

except ImportError: # pragma: no cover
_symbolics = False
from spatialmath.base import symbolic as sym

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

Expand Down Expand Up @@ -55,8 +48,8 @@ def norm(v: ArrayLikePure) -> float:
for x in v:
sum += x * x

if _symbolics and isinstance(sum, sympy.Expr):
return sympy.sqrt(sum)
if sym.issymbol(sum):
return sym.sqrt(sum)
else:
return math.sqrt(sum)

Expand Down
Loading