Source code for spatialmath.base.argcheck

# Part of Spatial Math Toolbox for Python
# Copyright (c) 2000 Peter Corke
# MIT Licence, see details in top-level file: LICENCE


"""
Utility functions for testing and converting passed arguments.  Used in all
spatialmath functions and classes to provides for flexibility in argument types 
that can be passed.
"""

# pylint: disable=invalid-name

import math
import numpy as np
from collections.abc import Iterable

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

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

# from typing import Union, List, Tuple, Any, Optional, Type, Callable
# from numpy.typing import DTypeLike
# Array = np.ndarray[Any, np.dtype[np.floating]]
# ArrayLike = Union[float,List[float],Tuple,Array]  # various ways to represent R^3 for input

from spatialmath.base.types import *


[docs]def isscalar(x: Any) -> bool: """ Test if argument is a real scalar :param x: value to test :return: whether value is a scalar :rtype: bool ``isscalar(x)`` is ``True`` if ``x`` is a Python or numPy int or real float. .. runblock:: pycon >>> from spatialmath.base import isscalar >>> isscalar(1) >>> isscalar(1.2) >>> isscalar([1]) """ return isinstance(x, _scalartypes)
[docs]def isinteger(x: Any) -> bool: """ Test if argument is a scalar integer :param x: value to test :return: whether value is a scalar :rtype: bool ``isinteger(x)`` is ``True`` if ``x`` is a Python or numPy int or real float. .. runblock:: pycon >>> from spatialmath.base import isscalar >>> isinteger(1) >>> isinteger(1.2) """ return isinstance(x, (int, np.integer))
[docs]def assertmatrix( m: Any, shape: Tuple[Union[int, None], Union[int, None]] = (None, None) ) -> None: """ Assert that argument is a 2D matrix :param m: value to test :param shape: required shape :type shape: 2-tuple :raises TypeError: if value is not a real Numpy array :raises ValueError: if value is not of the specified shape Tests if the argument is a real 2D matrix with a specified shape ``shape`` but the value ``None`` indicate an unspecified (wildcard, don't care) dimension. - ``assertsmatrix(A)`` raises an exception if ``m`` is not convertible to a 2D array - ``assertsmatrix(A, (N,M))`` as above but ``m`` must have shape (``N``,``M``) - ``assertsmatrix(A, (N,None))`` as above but ``m`` must have ``N`` rows - ``assertsmatrix(A, (None,M))`` as above but ``m`` must have ``M`` columns :seealso: :func:`ismatrix` """ if not isinstance(m, np.ndarray): raise TypeError("input must be a numPy ndarray") if m.dtype.kind == "c": raise TypeError("input must be a real numPy ndarray") if shape is not None: if len(shape) != len(m.shape): raise ValueError( "incorrect scalar of matrix dimensions, expecting {}, got {}".format( shape, m.shape ) ) if shape[0] is not None and shape[0] > 0 and shape[0] != m.shape[0]: raise ValueError( "incorrect matrix dimensions, expecting {}, got {}".format( shape, m.shape ) ) if ( len(shape) > 1 and shape[1] is not None and shape[1] > 0 and shape[1] != m.shape[1] ): raise ValueError( "incorrect matrix dimensions, expecting {}, got {}".format( shape, m.shape ) )
[docs]def ismatrix(m: Any, shape: Tuple[Union[int, None], Union[int, None]]) -> bool: """ Test if argument is a real 2D matrix :param m: value to test :param shape: required shape :type shape: 2-tuple :return: True if value is of specified shape :rtype: bool Tests if the argument is a real 2D matrix with a specified shape ``shape`` but the value ``None`` indicate an unspecified (wildcard, don't care) dimension, for example: .. runblock:: pycon >>> from spatialmath.base import ismatrix >>> import numpy as np >>> A = np.zeros((2,3)) >>> ismatrix(A, (2,3)) >>> ismatrix(A, (None,3)) >>> ismatrix(A, (2,None)) >>> ismatrix(A, (2,4)) .. note:: Unlike ``verifymatrix`` this function: - checks the argument is real valued - allows the shape to have an unspecified dimension :seealso: :func:`getmatrix`, :func:`verifymatrix`, :func:`assertmatrix` """ if not isinstance(m, np.ndarray): return False if m.dtype.kind == "c": return False if len(shape) != len(m.shape): return False if shape[0] is not None and shape[0] > 0 and shape[0] != m.shape[0]: return False if shape[1] is not None and shape[1] > 0 and shape[1] != m.shape[1]: return False return True
[docs]def getmatrix( m: ArrayLike, shape: Tuple[Union[int, None], Union[int, None]], dtype: DTypeLike = np.float64, ) -> np.ndarray: r""" Convert argument to 2D array :param m: input value :param shape: shape of returned matrix :type shape: 2-tuple :raises ValueError: if ``m`` is inconsistent with ``shape`` :raises TypeError: if ``m`` is not required type :return: a 2D array :rtype: NumPy ndarray :raises TypeError: if value is not a scalar or Numpy array :raises ValueError: if value is not of the specified shape ``getmatrix(m, shape)`` is a 2D matrix with shape ``shape`` formed from ``m`` which can be a 2D array, 1D array-like or a scalar. .. runblock:: pycon >>> from spatialmath.base import getmatrix >>> import numpy as np >>> getmatrix(3, (1,1)) >>> getmatrix([3,4], (1,2)) >>> getmatrix([3,4], (2, 1)) >>> getmatrix([3,4,5,6], (2,2)) >>> getmatrix(np.r_[3,4,5,6], (2,2)) .. note:: - If ``m`` is a 2D array its shape is compared to ``shape`` - a 2-tuple where ``None`` stands for unspecified, ie. ``(None, 2)`` will match any array where the second dimension is 2. - If ``m`` is a 1D array its shape is checked to see if it can be reshaped to ``shape``. A n-array could be reshaped as (n,1) or (1,n) or any other shape with the correct number of elements. A value of ``None`` in the shape stands for unspecified, ie. ``(None, 2)`` will attempt to reshape ``m`` as an array with shape (k,2) where :math:`k \times 2 \eq n`. - If ``m`` is a scalar, return an array of shape (1,1) :seealso: :func:`ismatrix`, :func:`verifymatrix` :SymPy: supported """ if isinstance(m, np.ndarray) and len(m.shape) == 2: # passed a 2D array mshape = m.shape if m.dtype == "O": dtype = "O" if (shape[0] is None or shape[0] == mshape[0]) and ( shape[1] is None or shape[1] == mshape[1] ): return np.array(m, dtype=dtype) else: raise ValueError(f"expecting {shape} but got {mshape}") elif isvector(m): # passed a 1D array m = getvector(m, dtype=dtype, out="array") if shape[0] is not None and shape[1] is not None: shape = cast(Tuple[int, int], shape) if len(m) == np.prod(shape): return m.reshape(shape) else: raise ValueError("array cannot be reshaped") elif shape[0] is not None and shape[1] is None: return m.reshape((shape[0], -1)) elif shape[0] is None and shape[1] is not None: return m.reshape((-1, shape[1])) else: return m.reshape((1, -1)) else: raise TypeError("argument must be scalar or ndarray")
[docs]def verifymatrix( m: np.ndarray, shape: Tuple[Union[int, None], Union[int, None]] ) -> None: """ Assert that argument is array of specified size :param m: value to be tested :param shape: desired shape of value :type shape: 2-tuple :raises TypeError: argument is not a NumPy array :raises ValueError: argument has incorrect shape Raises an exception if the argument ``m`` is not a NumPy array of the specified shape. .. note:: Unlike ``assertmatrix`` the specified shape cannot have wildcard dimensions. :seealso: :func:`assertmatrix`,:func:`getmatrix`, :func:`ismatrix` """ if not isinstance(m, np.ndarray): raise TypeError("input must be a numPy ndarray") if not m.shape == shape: raise ValueError("incorrect matrix dimensions, expecting {0}".format(shape))
# and not np.iscomplex(m) checks every element, would need to be not np.any(np.iscomplex(m)) which seems expensive @overload def getvector( v: ArrayLike, dim: Optional[Union[int, None]] = None, out: str = "array", dtype: DTypeLike = np.float64, ) -> NDArray: ... @overload def getvector( v: ArrayLike, dim: Optional[Union[int, None]] = None, out: str = "list", dtype: DTypeLike = np.float64, ) -> List[float]: ... @overload def getvector( v: Tuple[float, ...], dim: Optional[Union[int, None]] = None, out: str = "sequence", dtype: DTypeLike = np.float64, ) -> Tuple[float, ...]: ... @overload def getvector( v: List[float], dim: Optional[Union[int, None]] = None, out: str = "sequence", dtype: DTypeLike = np.float64, ) -> List[float]: ...
[docs]def getvector( v: ArrayLike, dim: Optional[Union[int, None]] = None, out: str = "array", dtype: DTypeLike = np.float64, ) -> Union[NDArray, List[float], Tuple[float, ...]]: """ Return a vector value :param v: passed vector :param dim: required dimension, or None if any length is ok :type dim: int or None :param out: output format, default is 'array' :type out: str :param dtype: datatype for numPy array return (default np.float64) :type dtype: numPy type :return: vector value in specified format :raises TypeError: value is not a list or NumPy array :raises ValueError: incorrect number of elements - ``getvector(vec)`` is ``vec`` converted to the output format ``out`` where ``vec`` is any of: - a Python native int or float, a 1-vector - Python native list or tuple - numPy real 1D array, ie. shape=(N,) - numPy real 2D array with a singleton dimension, ie. shape=(1,N) or (N,1) - ``getvector(vec, N)`` as above but must be an ``N``-element vector. The returned vector will be in the format specified by ``out``: ========== =============================================== format return type ========== =============================================== 'sequence' Python list, or tuple if a tuple was passed in 'list' Python list 'array' 1D numPy array, shape=(N,) [default] 'row' row vector, a 2D numPy array, shape=(1,N) 'col' column vector, 2D numPy array, shape=(N,1) ========== =============================================== .. runblock:: pycon >>> from spatialmath.base import getvector >>> import numpy as np >>> getvector([1,2]) # list >>> getvector([1,2], out='row') # list >>> getvector([1,2], out='col') # list >>> getvector((1,2)) # tuple >>> getvector(np.r_[1,2,3], out='sequence') # numpy array >>> getvector(1) # scalar >>> getvector([1]) >>> getvector([[1]]) >>> getvector([1,2], 2) >>> # getvector([1,2], 3) --> ValueError .. note:: - For 'array', 'row' or 'col' output the NumPy dtype defaults to the ``dtype`` of ``v`` if it is a NumPy array, otherwise it is set to the value specified by the ``dtype`` keyword which defaults to ``np.float64``. - If ``v`` is symbolic the ``dtype`` is retained as ``'O'`` :seealso: :func:`isvector` """ dt = dtype if isinstance(v, _scalartypes): # handle scalar case v = [v] # type: ignore if isinstance(v, (list, tuple)): # list or tuple was passed in if issymbol(v): dt = None if dim is not None and v and len(v) != dim: raise ValueError( "incorrect vector length: expected {}, got {}".format(dim, len(v)) ) if out == "sequence": return v elif out == "list": return list(v) elif out == "array": return np.array(v, dtype=dt) elif out == "row": return np.array(v, dtype=dt).reshape(1, -1) elif out == "col": return np.array(v, dtype=dt).reshape(-1, 1) else: raise ValueError("invalid output specifier") elif isinstance(v, np.ndarray): s = v.shape if dim is not None: if not (s == (dim,) or s == (1, dim) or s == (dim, 1)): raise ValueError( "incorrect vector length: expected {}, got {}".format(dim, s) ) v = v.flatten() if v.dtype.kind == "O": dt = "O" if out in ("sequence", "list"): return list(v.flatten()) elif out == "array": return v.astype(dt) elif out == "row": return v.astype(dt).reshape(1, -1) elif out == "col": return v.astype(dt).reshape(-1, 1) else: raise ValueError("invalid output specifier") else: raise TypeError("invalid input type")
[docs]def assertvector( v: Any, dim: Optional[Union[int, None]] = None, msg: Optional[str] = None ) -> None: """ Assert that argument is a real vector :param v: passed vector :param dim: required dimension :type dim: int or None :raises ValueError: if not a vector of specified length - ``assertvector(vec)`` raise an exception if ``vec`` is not a vector, ie. it is not any of: - a Python native int or float, a 1-vector - Python native list or tuple - numPy real 1D array, ie. shape=(N,) - numPy real 2D array with a singleton dimension, ie. shape=(1,N) or (N,1) - ``assertvector(vec, N)`` as above but must also check the length is ``N``. :seealso: :func:`getvector`, :func:`isvector` """ if not isvector(v, dim): raise ValueError(msg)
[docs]def isvector(v: Any, dim: Optional[int] = None) -> bool: """ Test if argument is a real vector :param v: value to test :param dim: required dimension :type dim: int or None :return: whether value is a valid vector :rtype: bool - ``isvector(vec)`` is ``True`` if ``vec`` is a vector, ie. any of: - a Python native int or float, a 1-vector - Python native list or tuple - numPy real 1D array, ie. shape=(N,) - numPy real 2D array with a singleton dimension, ie. shape=(1,N) or (N,1) - ``isvector(vec, N)`` as above but must also be an ``N``-element vector. .. runblock:: pycon >>> from spatialmath.base import isvector >>> import numpy as np >>> isvector([1,2]) # list >>> isvector((1,2)) # tuple >>> isvector(np.r_[1,2,3]) # numpy array >>> isvector(1) # scalar >>> isvector([1,2], 3) # list :seealso: :func:`getvector`, :func:`assertvector` """ if ( isinstance(v, (list, tuple)) and (dim is None or len(v) == dim) and all(map(lambda x: isinstance(x, _scalartypes), v)) ): return True # list or tuple if isinstance(v, np.ndarray): s = v.shape if dim is None: return ( (len(s) == 1 and s[0] > 0) or (s[0] == 1 and s[1] > 0) or (s[0] > 0 and s[1] == 1) ) else: return s == (dim,) or s == (1, dim) or s == (dim, 1) if (dim is None or dim == 1) and isinstance(v, _scalartypes): return True return False
[docs]def getunit(v: ArrayLike, unit: str = "rad", dim=None) -> Union[float, NDArray]: """ Convert values according to angular units :param v: the value in radians or degrees :type v: array_like(m) :param unit: the angular unit, "rad" or "deg" :type unit: str :param dim: expected dimension of input, defaults to None :type dim: int, optional :return: the converted value in radians :rtype: ndarray(m) or float :raises ValueError: argument is not a valid angular unit The input value is assumed to be in units of ``unit`` and is converted to radians. .. runblock:: pycon >>> from spatialmath.base import getunit >>> import numpy as np >>> getunit(1.5, 'rad') >>> getunit(1.5, 'rad', dim=0) >>> # getunit([1.5], 'rad', dim=0) --> ValueError >>> getunit(90, 'deg') >>> getunit([90, 180], 'deg') >>> getunit(np.r_[0.5, 1], 'rad') >>> getunit(np.r_[90, 180], 'deg') >>> getunit(np.r_[90, 180], 'deg', dim=2) >>> # getunit([90, 180], 'deg', dim=3) --> ValueError :note: - the input value is processed by :func:`getvector` and the argument ``dim`` can be used to check that ``v`` is the desired length. - the output is always an ndarray except if the input is a scalar and ``dim=0``. :seealso: :func:`getvector` """ if not isinstance(v, Iterable) and dim == 0: # scalar in, scalar out if unit == "rad": return v elif unit == "deg": return np.deg2rad(v) else: raise ValueError("invalid angular units") else: # scalar or iterable in, ndarray out # iterable passed in if dim == 0: raise ValueError("for dim==0 input must be a scalar") v = getvector(v, dim=dim) if unit == "rad": return v elif unit == "deg": return np.deg2rad(v) else: raise ValueError("invalid angular units")
[docs]def isnumberlist(x: Any) -> bool: """ Test if argument is a list of scalars :param x: the value to test :return: True if the argument is a list of real scalars :rtype: bool ``isscalarlist(x)`` is ``True`` if ``x```` is a list of scalars. .. runblock:: pycon >>> from spatialmath.base import isnumberlist >>> import numpy as np >>> isnumberlist((1,2,3)) >>> isnumberlist([1.1, 2.2, 3.3]) >>> isnumberlist(1) >>> isnumberlist(np.r_[1,2]) """ return ( isinstance(x, (list, tuple)) and len(x) > 0 and all(map(lambda x: isinstance(x, _scalartypes), x)) )
[docs]def isvectorlist(x: Any, n: int) -> bool: """ Test if argument is a list of vectors :param x: the value to test :return: True if the argument is a list of n-vectors :rtype: bool ``isvectorlist(x, n)`` is ``True`` if ``x`` is a list or tuple of 1D numPy arrays of shape=(n,). .. runblock:: pycon >>> from spatialmath.base import isvectorlist >>> import numpy as np >>> isvectorlist([np.r_[1,2], np.r_[3,4], np.r_[5,6]], 2) >>> isvectorlist([(1,2), (3,4), (5,6)], 2) >>> isvectorlist([np.r_[1,2], np.r_[3,4], np.r_[5,6,7]], 2) """ return islistof(x, lambda x: isinstance(x, np.ndarray) and x.shape == (n,))
[docs]def islistof(value: Any, what: Union[Type, Callable], n: Optional[int] = None): """ Test if argument is a list of specified type :param value: the value to test :type value: list or tuple :param what: type, tuple of types or function :type what: type or callable :param n: length of list, defaults to None :type n: int, optional :return: whether ``value`` is a specified list :rtype: bool Tests that every element of ``value`` is of the desired type. The type is specified by ``what`` and can be: * a single type, eg. ``int`` * a tuple of types, eg. ``(int, float)`` * a reference to a function which is passed each elemnent of the list and returns True if it is a valid member of the list. The length of the list can also be tested by specifying the argument ``n``. .. runblock:: pycon >>> from spatialmath.base import islistof >>> a = [3, 4, 5] >>> islistof(a, int) >>> islistof(a, int, 2) >>> a = [3, 4.5, 5.6] >>> islistof(a, int) >>> islistof(a, (int, float)) >>> a = [[1,2], [3, 4], [5,6]] >>> islistof(a, lambda x: islistof(x, int, 2)) """ if not isinstance(value, (list, tuple)): return False if n is not None and len(value) != n: return False if isinstance(what, type) or isinstance(what, tuple): # it's a type or tuple of types return all([isinstance(x, what) for x in value]) elif callable(what): return all([what(x) for x in value]) else: raise ValueError("bad value of what")
if __name__ == "__main__": import pathlib exec( open( pathlib.Path(__file__).parent.parent.parent.absolute() / "tests" / "base" / "test_argcheck.py" ).read() ) # pylint: disable=exec-used