Source code for bdsim.blocks.linalg

"""
Linear algebra blocks:

- have inputs and outputs
- have no state variables
- are a subclass of ``FunctionBlock`` |rarr| ``Block``

"""

# The constructor of each class ``MyClass`` with a ``@block`` decorator becomes a method ``MYCLASS()`` of the BlockDiagram instance.

import numpy as np
import math

from bdsim.components import FunctionBlock


[docs]class Inverse(FunctionBlock): r""" :blockname:`INVERSE` Matrix inverse. :inputs: 1 :outputs: 2 :states: 0 .. list-table:: :header-rows: 1 * - Port type - Port number - Types - Description * - Input - 0 - ndarray - :math:`\mathbf{A}` * - Output - 0 - ndarray - :math:`\mathbf{A}^{-1}` * - Output - 1 - float - :math:`\mbox{cond}(\mathbf{A})` Compute inverse of the 2D-array input signal. If the matrix is square the inverse is computed unless the ``pinv`` flag is True. For a non-square matrix the pseudo-inverse is used. The condition number is output on the second port. :seealso: `numpy.linalg.inv <https://numpy.org/doc/stable/reference/generated/numpy.linalg.inv.html>`_, `numpy.linalg.pinv <https://numpy.org/doc/stable/reference/generated/numpy.linalg.pinv.html>`_, `numpy.linalg.cond <https://numpy.org/doc/stable/reference/generated/numpy.linalg.cond.html>`_ """ nin = 1 nout = 2 onames = ("inv", "cond")
[docs] def __init__(self, pinv=False, **blockargs): """ :param pinv: force pseudo inverse, defaults to False :type pinv: bool, optional :param blockargs: |BlockOptions| :type blockargs: dict """ super().__init__(**blockargs) self.type = "inverse" self.pinv = pinv
def output(self, t, inports, x): mat = inports[0] if isinstance(mat, np.ndarray): if mat.shape[0] != mat.shape[1]: pinv = True else: pinv = self.pinv if pinv: out = np.linalg.pinv(mat) else: try: out = np.linalg.inv(mat) except np.linalg.LinAlgError: raise RuntimeError("matrix is singular") return [out, np.linalg.cond(mat)] elif hasattr(mat, "inv"): # ask the object to invert itself return [mat.inv(), None] else: raise RuntimeError("object cannot be inverted")
# ------------------------------------------------------------------------ #
[docs]class Transpose(FunctionBlock): r""" :blockname:`TRANSPOSE` Matrix transpose. :inputs: 1 :outputs: 2 :states: 0 .. list-table:: :header-rows: 1 * - Port type - Port number - Types - Description * - Input - 0 - ndarray - :math:`\mathbf{A}` * - Output - 0 - ndarray - :math:`\mathbf{A}^{\top}` Compute transpose of the 2D-array input signal. .. note:: - An input 1D-array of shape (N,) is turned into a 2D-array column vector with shape (N,1). - An input 2D-array column vector of shape (N,1) becomes a 2D-array row vector with shape (1,N). :seealso: `numpy.linalg.transpose <https://numpy.org/doc/stable/reference/generated/numpy.linalg.transpose.html>`_ """ nin = 1 nout = 1
[docs] def __init__(self, **blockargs): """ :param blockargs: |BlockOptions| :type blockargs: dict """ super().__init__(**blockargs) self.type = "transpose"
def output(self, t, inports, x): mat = inports[0] if mat.ndim == 1: out = mat.reshape((mat.shape[0], 1)) else: out = mat.T return [out]
# ------------------------------------------------------------------------ #
[docs]class Norm(FunctionBlock): r""" :blockname:`NORM` Array norm. :inputs: 1 :outputs: 1 :states: 0 .. list-table:: :header-rows: 1 * - Port type - Port number - Types - Description * - Input - 0 - ndarray - :math:`\mathbf{A}` * - Output - 0 - ndarray - :math:`\|\mathbf{A}\|` Computes the specified norm for a 1D- or 2D-array. :seealso: `numpy.linalg.norm <https://numpy.org/doc/stable/reference/generated/numpy.linalg.norm.html>`_ """ nin = 1 nout = 1
[docs] def __init__(self, ord=None, axis=None, **blockargs): """ :param axis: specifies the axis along which to compute the vector norms, defaults to None. :type axis: int, optional :param ord: Order of the norm, default to None. :type ord: int or str :param blockargs: |BlockOptions| :type blockargs: dict """ super().__init__(**blockargs) self.type = "norm" self.args = dict(ord=ord, axis=axis)
def output(self, t, inports, x): vec = inports[0] out = np.linalg.norm(vec, **self.args) return [out]
# ------------------------------------------------------------------------ #
[docs]class Flatten(FunctionBlock): r""" :blockname:`FLATTEN` Flatten a multi-dimensional array. :inputs: 1 :outputs: 1 :states: 0 .. list-table:: :header-rows: 1 * - Port type - Port number - Types - Description * - Input - 0 - ndarray - :math:`\mathbf{A}` * - Output - 0 - ndarray - :math:`\mbox{vec}(\mathbf{A})` Flattens the incoming array in either row major ('C') or column major ('F') order. :seealso: `numpy.flatten <https://numpy.org/doc/stable/reference/generated/numpy.flatten.html>`_ """ nin = 1 nout = 1
[docs] def __init__(self, order="C", **blockargs): """ :param order: flattening order, either "C" or "F", defaults to "C" :type order: str :param blockargs: |BlockOptions| :type blockargs: dict """ super().__init__(**blockargs) self.type = "flatten" self.order = order
def output(self, t, inports, x): vec = inports[0] out = vec.flatten(self.order) return [out]
# ------------------------------------------------------------------------ #
[docs]class Slice2(FunctionBlock): r""" :blockname:`SLICE2` Slice out subarray of 2D-array. :inputs: 1 :outputs: 1 :states: 0 .. list-table:: :header-rows: 1 * - Port type - Port number - Types - Description * - Input - 0 - ndarray - :math:`\mathbf{A}` * - Output - 0 - ndarray - :math:`\mathbf{A}_{i\ldots j, m\ldots n}` Compute a 2D slice of input 2D array. If ``rows`` or ``cols`` is ``None`` it means all rows or columns respectively. If ``rows`` or ``cols`` is a list, perform NumPy fancy indexing, returning the specified rows or columns Example:: slice = bd.SLICE2(rows=[2,3]) # return rows 2 and 3, all columns slice = bd.SLICE2(cols=[4,1]) # return columns 4 and 1, all rows slice = bd.SLICE2(rows=[2,3], cols=[4,1]) # return elements [2,4] and [3,1] as a 1D array If a single row or column is selected, the result will be a 1D array If ``rows`` or ``cols`` is a tuple, it must have three elements. It describes a Python slice ``(start, stop, step)`` where any element can be ``None`` * ``start=None`` means start at first element * ``stop=None`` means finish at last element * ``step=None`` means step by one ``rows=None`` is equivalent to ``rows=(None, None, None)``. Example:: slice = bd.SLICE2(rows=(None,None,2)) # return every second row slice = bd.SLICE2(cols=(None,None,-1)) # reverse the columns The list and tuple notation can be mixed, for example, one for rows and one for columns. :seealso: :class:`Slice1` :class:`Index` """ nin = 1 nout = 1
[docs] def __init__(self, rows=None, cols=None, **blockargs): """ :param rows: row selection, defaults to None :type rows: tuple(3) or list :param cols: column selection, defaults to None :type cols: tuple(3) or list :param blockargs: |BlockOptions| :type blockargs: dict """ super().__init__(**blockargs) self.type = "slice2" if rows is None: self.rows = slice(None, None, None) elif isinstance(rows, list): self.rows = rows elif isinstance(rows, tuple) and len(rows) == 3: self.rows = slice(*rows) else: raise ValueError("bad rows specifier") if cols is None: self.cols = slice(None, None, None) elif isinstance(cols, list): self.cols = cols elif isinstance(cols, tuple) and len(cols) == 3: self.cols = slice(*cols) else: raise ValueError("bad columns specifier")
def output(self, t, inports, x): array = inports[0] if array.ndim != 2: raise RuntimeError("Slice2 block expecting 2d array") return [array[self.rows, self.cols]]
# ------------------------------------------------------------------------ #
[docs]class Slice1(FunctionBlock): r""" :blockname:`SLICE1` Slice out subarray of 1D-array. :inputs: 1 :outputs: 1 :states: 0 .. list-table:: :header-rows: 1 * - Port type - Port number - Types - Description * - Input - 0 - ndarray - :math:`v` * - Output - 0 - ndarray - :math:`v_{i\dots j}` Compute a 1D slice of input 1D array. If ``index`` is ``None`` it means all elements. If ``index`` is a list, perform NumPy fancy indexing, returning the specified elements Example:: slice = bd.SLICE1(index=[2,3]) # return elements 2 and 3 as a 1D array slice = bd.SLICE1(index=[2]) # return element 2 as a 1D array slice = bd.SLICE1(index=2) # return element 2 as a NumPy scalar If ``index`` is a tuple, it must have three elements. It describes a Python slice ``(start, stop, step)`` where any element can be ``None`` * ``start=None`` means start at first element * ``stop=None`` means finish at last element * ``step=None`` means step by one ``rows=None`` is equivalent to ``rows=(None, None, None)``. Example:: slice = bd.SLICE1(index=(None,None,2)) # return every second element slice = bd.SLICE1(index=(None,None,-1)) # reverse the elements :seealso: :class:`Slice1` """ nin = 1 nout = 1
[docs] def __init__(self, index, **blockargs): """ :param index: slice, defaults to None :type index: tuple(3) :param blockargs: |BlockOptions| :type blockargs: dict """ super().__init__(**blockargs) self.type = "slice1" if index is None: self.index = slice(None, None, None) elif isinstance(index, list): self.index = index elif isinstance(index, tuple) and len(index) == 3: self.index = slice(*index) else: raise ValueError("bad index specifier")
def output(self, t, inports, x): array = inports[0] if array.ndim != 1: raise RuntimeError("Slice1 block expecting 1d array") return [array[self.index]]
# ------------------------------------------------------------------------ #
[docs]class Det(FunctionBlock): r""" :blockname:`DET` Matrix determinant. :inputs: 1 :outputs: 1 :states: 0 .. list-table:: :header-rows: 1 * - Port type - Port number - Types - Description * - Input - 0 - ndarray - :math:`\mathbf{A}` * - Output - 0 - ndarray - :math:`\mbox{det}(\mathbf{A})` Compute the matrix determinant. :seealso: `numpy.linalg.det <https://numpy.org/doc/stable/reference/generated/numpy.linalg.det.html>`_ """ nin = 1 nout = 1
[docs] def __init__(self, **blockargs): """ :param blockargs: |BlockOptions| :type blockargs: dict """ super().__init__(**blockargs) self.type = "det"
def output(self, t, inports, x): mat = inports[0] out = np.linalg.det(mat) return [out]
# ------------------------------------------------------------------------ #
[docs]class Cond(FunctionBlock): r""" :blockname:`COND` Matrix condition number. :inputs: 1 :outputs: 1 :states: 0 .. list-table:: :header-rows: 1 * - Port type - Port number - Types - Description * - Input - 0 - ndarray - :math:`\mathbf{A}` * - Output - 0 - ndarray - :math:`\mbox{cond}(\mathbf{A})` :seealso: `numpy.linalg.cond <https://numpy.org/doc/stable/reference/generated/numpy.linalg.cond.html>`_ """ nin = 1 nout = 1
[docs] def __init__(self, **blockargs): """ :param blockargs: |BlockOptions| :type blockargs: dict """ super().__init__(**blockargs) self.type = "cond"
def output(self, t, inports, x): mat = inports[0] out = np.linalg.cond(mat) return [out]
if __name__ == "__main__": # pragma: no cover from pathlib import Path exec(open(Path(__file__).parent.parent.parent / "tests" / "test_linalg.py").read())