Module transforms.transforms

Transforms

Expand source code
# SPDX-FileCopyrightText: © 2021 Antonio López Rivera <antonlopezr99@gmail.com>
# SPDX-License-Identifier: GPL-3.0-only

"""
Transforms
----------
"""


import numpy as np
import sympy as sp

from sympy.utilities.lambdify import lambdify

from alexandria.shell import print_color


class M:
    """
    General matrix class
    --------------------
    """
    def __init__(self, matrix, params):
        """
        :param matrix: Matrix
        :param params: Set of parameters
        """

        self.matrix = matrix
        self.params = params

    def __getitem__(self, item):
        """
        Retrieve
        """
        return self.matrix[item]

    def __getattr__(self, item):
        """
        Pass unknown attributes to matrix class
        """
        if item in self.__dict__.keys():
            return self.item
        else:
            try:
                self.matrix.__dict__[item]
            except KeyError:
                raise AttributeError(f"{self.matrix.__class__} has no attribute <{item}>")

    """
    Associated matrices
    """
    def C(self):
        """
        Counterrotation matrix
        ----------------------

        INERTIAL RF -> ROTATING RF
        """
        matrix = self.matrix
        # Negative angles
        for param in self.params:
            matrix = matrix.subs(param, f'-{param}')
        return M(matrix=matrix, params=self.params)

    def T(self):
        """
        Rotation matrix Transpose
        -------------------------

        ROTATION

        ROTATING RF -> INERTIAL RF
        """
        return M(matrix=self.matrix.T, params=self.params)

    def I(self):
        """
        Rotation matrix Inverse
        -----------------------

        COUNTERROTATION

        ROTATING RF -> INERTIAL RF
        """
        # Transpose
        inv_matrix = self.matrix.T
        # Negative angles
        for param in self.params:
            inv_matrix = inv_matrix.subs(param, f'-{param}')
        return M(matrix=inv_matrix, params=self.params)

    def diff(self):
        dm = sp.diff(self.matrix, self.params)
        return M(matrix=dm, params=self.params | dm.free_symbols)

    """
    Operators
    """
    def _operate(self, other, f):

        # Another rotation matrix
        if isinstance(other, M):
            return M(matrix=f(self.matrix, other.matrix), params=self.params | other.params)
        # A NumPy array
        elif isinstance(other, np.ndarray):
            return M(matrix=f(self.matrix, other), params=self.params)
        # A SymPy array
        elif 'sympy' in other.__module__:
            return M(matrix=f(self.matrix, other), params=self.params | other.free_symbols)
        else:
            raise ValueError("Multiplication input invalid. It should be either a rotation, or a NumPy or SymPy array.")

    def __mul__(self, other):
        return self._operate(other, lambda m1, m2: m1 * m2)

    def __add__(self, other):
        return self._operate(other, lambda m1, m2: m1 + m2)

    """
    Export
    """
    def __call__(self, *args, **kwargs):
        """
        Retrieve numerical rotation matrix
        ----------------------------------

        :param args:
            1. Single parameter
            2. List of matrix parameters
            3. List of NumPy arrays
                -> 1 array per parameter
        """

        f = lambdify(list(self.params), self.matrix)

        # Check arguments
        assert not (args and kwargs), \
            "Combination of non-keyword and keyword arguments not supported"

        arguments = []
        if args:
            arguments += args
        if kwargs:
            arguments += list(kwargs.values())

        assert len(arguments) == len(self.params), \
            f"Argument number mismatch. Arguments in the matrix:\n\n   {', '.join(str(p) for p in self.params)}"

        # Return functions
        def arg_rec_return(argument):

            # Input type
            a_tuple  = isinstance(argument, tuple)
            an_array = isinstance(argument, np.ndarray)

            # Return
            if a_tuple:
                if isinstance(argument[0], np.ndarray):
                    return [arg_rec_return(v) for v in np.vstack(argument).T]
                else:
                    return f(*argument)
            elif an_array:
                return [f(v) for v in argument]
            else:
                return f(*argument)

        def kwarg_rec_return(dictionary):

            # Argument type
            arrays = isinstance(list(dictionary.values())[0], np.ndarray)

            # Return
            if arrays:

                array_list = list(dictionary.values())
                array_size = array_list[0].size
                assert [a.size == array_size for a in array_list], \
                    "Input arrays do not have the same internal size."

                keys   = list(dictionary.keys())
                values = np.vstack(list(dictionary.values())).T

                return [f(**dict(zip(keys, list(values[i])))) for i in range(array_size)]

            else:
                return f(**kwargs)

        # Out
        if args:
            return arg_rec_return(args)
        if kwargs:
            return kwarg_rec_return(kwargs)

    """
    Representation
    """
    def __repr__(self):
        return sp.pretty(self.matrix)

    def latex(self, var="T"):
        lx = "\n"\
             r"\begin{equation}"                   + \
             f"\n   {var} = {sp.latex(self.matrix)}\n" + \
             r"\end{equation}"
        print_color(lx, "blue")
        return lx


class R(M):
    """
    Rotation Matrix
    ---------------
    """
    def __init__(self, delta):

        super(R, self).__init__(self._matrix(delta), {delta})


class Rx(R):
    """
    Rotation: X axis
    ----------------------
    """
    def _matrix(self, delta):
        return sp.Matrix([[1,  0,              0],
                          [0,  sp.cos(delta), -sp.sin(delta)],
                          [0,  sp.sin(delta),  sp.cos(delta)]])


class Ry(R):
    """
    Rotation: Y axis
    ----------------------
    """
    def _matrix(self, delta):
        return sp.Matrix([[sp.cos(delta),  0, sp.sin(delta)],
                          [0,              1, 0],
                          [-sp.sin(delta), 0, sp.cos(delta)]])


class Rz(R):
    """
    Rotation: Z axis
    ----------------------
    """
    def _matrix(self, delta):
        return sp.Matrix([[sp.cos(delta), -sp.sin(delta), 0],
                          [sp.sin(delta),  sp.cos(delta), 0],
                          [0,              0,             1]])


class T(R):
    """
    Transformation Matrix
    ---------------------

    INERTIAL RF -> ROTATING RF

    By convention, a transformation matrix maps the coordinates of
    objects in an INERTIAL REFERENCE FRAME to their coordinates in
    a ROTATING one. In other words:

        r_{Rotating RF} = T_{RI} * r_{Inertial RF}

    """
    pass


class Tx(T):
    """
    Transformation: X axis
    ----------------------
    """
    def _matrix(self, delta):
        return sp.Matrix([[1,  0,              0],
                          [0,  sp.cos(delta),  sp.sin(delta)],
                          [0, -sp.sin(delta),  sp.cos(delta)]])


class Ty(T):
    """
    Transformation: Y axis
    ----------------------
    """
    def _matrix(self, delta):
        return sp.Matrix([[sp.cos(delta), 0, -sp.sin(delta)],
                          [0,             1,  0],
                          [sp.sin(delta), 0,  sp.cos(delta)]])


class Tz(T):
    """
    Transformation: Z axis
    ----------------------
    """
    def _matrix(self, delta):
        return sp.Matrix([[sp.cos(delta),  sp.sin(delta), 0],
                          [-sp.sin(delta), sp.cos(delta), 0],
                          [0,              0,             1]])

Classes

class M (matrix, params)

General Matrix Class

:param matrix: Matrix :param params: Set of parameters

Expand source code
class M:
    """
    General matrix class
    --------------------
    """
    def __init__(self, matrix, params):
        """
        :param matrix: Matrix
        :param params: Set of parameters
        """

        self.matrix = matrix
        self.params = params

    def __getitem__(self, item):
        """
        Retrieve
        """
        return self.matrix[item]

    def __getattr__(self, item):
        """
        Pass unknown attributes to matrix class
        """
        if item in self.__dict__.keys():
            return self.item
        else:
            try:
                self.matrix.__dict__[item]
            except KeyError:
                raise AttributeError(f"{self.matrix.__class__} has no attribute <{item}>")

    """
    Associated matrices
    """
    def C(self):
        """
        Counterrotation matrix
        ----------------------

        INERTIAL RF -> ROTATING RF
        """
        matrix = self.matrix
        # Negative angles
        for param in self.params:
            matrix = matrix.subs(param, f'-{param}')
        return M(matrix=matrix, params=self.params)

    def T(self):
        """
        Rotation matrix Transpose
        -------------------------

        ROTATION

        ROTATING RF -> INERTIAL RF
        """
        return M(matrix=self.matrix.T, params=self.params)

    def I(self):
        """
        Rotation matrix Inverse
        -----------------------

        COUNTERROTATION

        ROTATING RF -> INERTIAL RF
        """
        # Transpose
        inv_matrix = self.matrix.T
        # Negative angles
        for param in self.params:
            inv_matrix = inv_matrix.subs(param, f'-{param}')
        return M(matrix=inv_matrix, params=self.params)

    def diff(self):
        dm = sp.diff(self.matrix, self.params)
        return M(matrix=dm, params=self.params | dm.free_symbols)

    """
    Operators
    """
    def _operate(self, other, f):

        # Another rotation matrix
        if isinstance(other, M):
            return M(matrix=f(self.matrix, other.matrix), params=self.params | other.params)
        # A NumPy array
        elif isinstance(other, np.ndarray):
            return M(matrix=f(self.matrix, other), params=self.params)
        # A SymPy array
        elif 'sympy' in other.__module__:
            return M(matrix=f(self.matrix, other), params=self.params | other.free_symbols)
        else:
            raise ValueError("Multiplication input invalid. It should be either a rotation, or a NumPy or SymPy array.")

    def __mul__(self, other):
        return self._operate(other, lambda m1, m2: m1 * m2)

    def __add__(self, other):
        return self._operate(other, lambda m1, m2: m1 + m2)

    """
    Export
    """
    def __call__(self, *args, **kwargs):
        """
        Retrieve numerical rotation matrix
        ----------------------------------

        :param args:
            1. Single parameter
            2. List of matrix parameters
            3. List of NumPy arrays
                -> 1 array per parameter
        """

        f = lambdify(list(self.params), self.matrix)

        # Check arguments
        assert not (args and kwargs), \
            "Combination of non-keyword and keyword arguments not supported"

        arguments = []
        if args:
            arguments += args
        if kwargs:
            arguments += list(kwargs.values())

        assert len(arguments) == len(self.params), \
            f"Argument number mismatch. Arguments in the matrix:\n\n   {', '.join(str(p) for p in self.params)}"

        # Return functions
        def arg_rec_return(argument):

            # Input type
            a_tuple  = isinstance(argument, tuple)
            an_array = isinstance(argument, np.ndarray)

            # Return
            if a_tuple:
                if isinstance(argument[0], np.ndarray):
                    return [arg_rec_return(v) for v in np.vstack(argument).T]
                else:
                    return f(*argument)
            elif an_array:
                return [f(v) for v in argument]
            else:
                return f(*argument)

        def kwarg_rec_return(dictionary):

            # Argument type
            arrays = isinstance(list(dictionary.values())[0], np.ndarray)

            # Return
            if arrays:

                array_list = list(dictionary.values())
                array_size = array_list[0].size
                assert [a.size == array_size for a in array_list], \
                    "Input arrays do not have the same internal size."

                keys   = list(dictionary.keys())
                values = np.vstack(list(dictionary.values())).T

                return [f(**dict(zip(keys, list(values[i])))) for i in range(array_size)]

            else:
                return f(**kwargs)

        # Out
        if args:
            return arg_rec_return(args)
        if kwargs:
            return kwarg_rec_return(kwargs)

    """
    Representation
    """
    def __repr__(self):
        return sp.pretty(self.matrix)

    def latex(self, var="T"):
        lx = "\n"\
             r"\begin{equation}"                   + \
             f"\n   {var} = {sp.latex(self.matrix)}\n" + \
             r"\end{equation}"
        print_color(lx, "blue")
        return lx

Subclasses

Methods

def C(self)

Counterrotation Matrix

INERTIAL RF -> ROTATING RF

Expand source code
def C(self):
    """
    Counterrotation matrix
    ----------------------

    INERTIAL RF -> ROTATING RF
    """
    matrix = self.matrix
    # Negative angles
    for param in self.params:
        matrix = matrix.subs(param, f'-{param}')
    return M(matrix=matrix, params=self.params)
def I(self)

Rotation Matrix Inverse

COUNTERROTATION

ROTATING RF -> INERTIAL RF

Expand source code
def I(self):
    """
    Rotation matrix Inverse
    -----------------------

    COUNTERROTATION

    ROTATING RF -> INERTIAL RF
    """
    # Transpose
    inv_matrix = self.matrix.T
    # Negative angles
    for param in self.params:
        inv_matrix = inv_matrix.subs(param, f'-{param}')
    return M(matrix=inv_matrix, params=self.params)
def T(self)

Rotation Matrix Transpose

ROTATION

ROTATING RF -> INERTIAL RF

Expand source code
def T(self):
    """
    Rotation matrix Transpose
    -------------------------

    ROTATION

    ROTATING RF -> INERTIAL RF
    """
    return M(matrix=self.matrix.T, params=self.params)
def diff(self)
Expand source code
def diff(self):
    dm = sp.diff(self.matrix, self.params)
    return M(matrix=dm, params=self.params | dm.free_symbols)
def latex(self, var='T')
Expand source code
def latex(self, var="T"):
    lx = "\n"\
         r"\begin{equation}"                   + \
         f"\n   {var} = {sp.latex(self.matrix)}\n" + \
         r"\end{equation}"
    print_color(lx, "blue")
    return lx
class R (delta)

Rotation Matrix

:param matrix: Matrix :param params: Set of parameters

Expand source code
class R(M):
    """
    Rotation Matrix
    ---------------
    """
    def __init__(self, delta):

        super(R, self).__init__(self._matrix(delta), {delta})

Ancestors

Subclasses

Inherited members

class Rx (delta)

Rotation: X axis

:param matrix: Matrix :param params: Set of parameters

Expand source code
class Rx(R):
    """
    Rotation: X axis
    ----------------------
    """
    def _matrix(self, delta):
        return sp.Matrix([[1,  0,              0],
                          [0,  sp.cos(delta), -sp.sin(delta)],
                          [0,  sp.sin(delta),  sp.cos(delta)]])

Ancestors

Inherited members

class Ry (delta)

Rotation: Y axis

:param matrix: Matrix :param params: Set of parameters

Expand source code
class Ry(R):
    """
    Rotation: Y axis
    ----------------------
    """
    def _matrix(self, delta):
        return sp.Matrix([[sp.cos(delta),  0, sp.sin(delta)],
                          [0,              1, 0],
                          [-sp.sin(delta), 0, sp.cos(delta)]])

Ancestors

Inherited members

class Rz (delta)

Rotation: Z axis

:param matrix: Matrix :param params: Set of parameters

Expand source code
class Rz(R):
    """
    Rotation: Z axis
    ----------------------
    """
    def _matrix(self, delta):
        return sp.Matrix([[sp.cos(delta), -sp.sin(delta), 0],
                          [sp.sin(delta),  sp.cos(delta), 0],
                          [0,              0,             1]])

Ancestors

Inherited members

class T (delta)

Transformation Matrix

INERTIAL RF -> ROTATING RF

By convention, a transformation matrix maps the coordinates of objects in an INERTIAL REFERENCE FRAME to their coordinates in a ROTATING one. In other words:

r_{Rotating RF} = T_{RI} * r_{Inertial RF}

:param matrix: Matrix :param params: Set of parameters

Expand source code
class T(R):
    """
    Transformation Matrix
    ---------------------

    INERTIAL RF -> ROTATING RF

    By convention, a transformation matrix maps the coordinates of
    objects in an INERTIAL REFERENCE FRAME to their coordinates in
    a ROTATING one. In other words:

        r_{Rotating RF} = T_{RI} * r_{Inertial RF}

    """
    pass

Ancestors

Subclasses

Inherited members

class Tx (delta)

Transformation: X axis

:param matrix: Matrix :param params: Set of parameters

Expand source code
class Tx(T):
    """
    Transformation: X axis
    ----------------------
    """
    def _matrix(self, delta):
        return sp.Matrix([[1,  0,              0],
                          [0,  sp.cos(delta),  sp.sin(delta)],
                          [0, -sp.sin(delta),  sp.cos(delta)]])

Ancestors

Inherited members

class Ty (delta)

Transformation: Y axis

:param matrix: Matrix :param params: Set of parameters

Expand source code
class Ty(T):
    """
    Transformation: Y axis
    ----------------------
    """
    def _matrix(self, delta):
        return sp.Matrix([[sp.cos(delta), 0, -sp.sin(delta)],
                          [0,             1,  0],
                          [sp.sin(delta), 0,  sp.cos(delta)]])

Ancestors

Inherited members

class Tz (delta)

Transformation: Z axis

:param matrix: Matrix :param params: Set of parameters

Expand source code
class Tz(T):
    """
    Transformation: Z axis
    ----------------------
    """
    def _matrix(self, delta):
        return sp.Matrix([[sp.cos(delta),  sp.sin(delta), 0],
                          [-sp.sin(delta), sp.cos(delta), 0],
                          [0,              0,             1]])

Ancestors

Inherited members