"""
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):
"""
:blockname:`INVERSE`
.. table::
:align: left
+----------+---------+---------+
| inputs | outputs | states |
+----------+---------+---------+
| 1 | 2 | 0 |
+----------+---------+---------+
| A(M,N) | A(N,M) | |
| | float | |
+----------+---------+---------+
"""
nin = 1
nout = 2
onames = ('inv', 'cond')
[docs] def __init__(self, pinv=False, **blockargs):
"""
Matrix inverse.
:param pinv: force pseudo inverse, defaults to False
:type pinv: bool, optional
:param blockargs: |BlockOptions|
:type blockargs: dict
:return: An INVERSE block
:rtype: Inverse instance
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: :func:`numpy.linalg.inv` :func:`numpy.linalg.pinv` :func:`numpy.linalg.cond`
"""
super().__init__(**blockargs)
self.type = 'inverse'
self.pinv = pinv
def output(self, t=None):
mat = self.inputs[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 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):
"""
:blockname:`TRANSPOSE`
.. table::
:align: left
+------------+---------+---------+
| inputs | outputs | states |
+------------+---------+---------+
| 1 | 1 | 0 |
+------------+---------+---------+
| A(M,N) | A(N,M) | |
+------------+---------+---------+
"""
nin = 1
nout = 1
[docs] def __init__(self, **blockargs):
"""
Matrix transpose.
:param blockargs: |BlockOptions|
:type blockargs: dict
:return: A TRANSPOSE block
:rtype: Transpose instance
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: :func:`numpy.transpose`
"""
super().__init__(**blockargs)
self.type = 'transpose'
def output(self, t=None):
mat = self.inputs[0]
if ndim == 1:
out = mat.reshape((mat.shape[0], 1))
else:
out = mat.T
return [out]
# ------------------------------------------------------------------------ #
[docs]class Norm(FunctionBlock):
"""
:blockname:`NORM`
.. table::
:align: left
+------------+---------+---------+
| inputs | outputs | states |
+------------+---------+---------+
| 1 | 1 | 0 |
+------------+---------+---------+
| A(N,) | float | |
| A(N,M) | | |
+------------+---------+---------+
"""
nin = 1
nout = 1
[docs] def __init__(self, ord=None, axis=None, **blockargs):
"""
Array norm.
: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
:return: A NORM block
:rtype: Norm instance
Computes the specified norm for a 1D- or 2D-array.
:seealso: :func:`numpy.linalg.norm`
"""
super().__init__(**blockargs)
self.type = 'norm'
self.args = dict(ord=ord, axis=axis)
def output(self, t=None):
vec = self.inputs[0]
out = np.linalg.norm(vec, **self.args)
return [out]
# ------------------------------------------------------------------------ #
[docs]class Flatten(FunctionBlock):
"""
:blockname:`FLATTEN`
.. table::
:align: left
+------------+---------+---------+
| inputs | outputs | states |
+------------+---------+---------+
| 1 | 1 | 0 |
+------------+---------+---------+
| A(N,M ) | A(NM,) | |
+------------+---------+---------+
"""
nin = 1
nout = 1
[docs] def __init__(self, order='C', **blockargs):
"""
Flatten a multi-dimensional array.
:param order: flattening order, either "C" or "F", defaults to "C"
:type order: str
:param blockargs: |BlockOptions|
:type blockargs: dict
:return: A FLATTEN block
:rtype: Flatten instance
Flattens the incoming array in either row major ('C') or column major ('F') order.
:seealso: :func:`numpy.flatten`
"""
super().__init__(**blockargs)
self.type = 'flatten'
self.order = order
def output(self, t=None):
vec = self.inputs[0]
out = vec.flatten(self.order)
return [out]
# ------------------------------------------------------------------------ #
[docs]class Slice2(FunctionBlock):
"""
:blockname:`SLICE2`
.. table::
:align: left
+------------+---------+---------+
| inputs | outputs | states |
+------------+---------+---------+
| 1 | 1 | 0 |
+------------+---------+---------+
| A(N,M) | A(K,L) | |
+------------+---------+---------+
"""
nin = 1
nout = 1
[docs] def __init__(self, rows=None, cols=None, **blockargs):
"""
Slice out subarray of 2D-array.
: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
:return: A SLICE2 block
:rtype: Slice2 instance
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::
SLICE2(rows=[2,3]) # return rows 2 and 3, all columns
SLICE2(cols=[4,1]) # return columns 4 and 1, all rows
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::
SLICE2(rows=(None,None,2)) # return every second row
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`
"""
super().__init__(**blockargs)
self.type = 'slice2'
if rows is None:
self.rows = slice()
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()
elif isinstance(cols, list):
self.rows = cols
elif isinstance(cols, tuple) and len(cols) == 3:
self.cols = slice(*cols)
else:
raise ValueError('bad rows specifier')
def output(self, t=None):
array = self.inputs[0]
if array.ndim != 2:
raise RuntimeError('flatten2 block expecting 2d array')
return [out[rows, cols]]
# ------------------------------------------------------------------------ #
[docs]class Slice1(FunctionBlock):
"""
:blockname:`SLICE1`
.. table::
:align: left
+------------+---------+---------+
| inputs | outputs | states |
+------------+---------+---------+
| 1 | 1 | 0 |
+------------+---------+---------+
| A(N) | A(M) | |
+------------+---------+---------+
"""
nin = 1
nout = 1
[docs] def __init__(self, index, **blockargs):
"""
Slice out subarray of 1D-array.
:param index: slice, defaults to None
:type index: tuple(3)
:param blockargs: |BlockOptions|
:type blockargs: dict
:return: A SLICE1 block
:rtype: Slice1 instance
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::
SLICE1(index=[2,3]) # return elements 2 and 3 as a 1D array
SLICE1(index=[2]) # return element 2 as a 1D array
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::
SLICE1(index=(None,None,2)) # return every second element
SLICE1(index=(None,None,-1)) # reverse the elements
:seealso: :class:`Slice1`
"""
super().__init__(**blockargs)
self.type = 'slice1'
if index is None:
self.index = slice()
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=None):
array = self.inputs[0]
if array.ndim != 1:
raise RuntimeError('flatten1 block expecting 1d array')
return [array[self.index]]
# ------------------------------------------------------------------------ #
[docs]class Det(FunctionBlock):
"""
:blockname:`DET`
.. table::
:align: left
+------------+---------+---------+
| inputs | outputs | states |
+------------+---------+---------+
| 1 | 1 | 0 |
+------------+---------+---------+
| A(N,N) | float | |
+------------+---------+---------+
"""
nin = 1
nout = 1
[docs] def __init__(self, **blockargs):
"""
Matrix determinant
:param blockargs: |BlockOptions|
:type blockargs: dict
:return: A DET block
:rtype: Det instance
Compute the matrix determinant.
:seealso: :func:`numpy.linalg.inv`
"""
super().__init__(**blockargs)
self.type = 'det'
def output(self, t=None):
mat = self.inputs[0]
out = np.linalg.det(mat)
return [mat]
# ------------------------------------------------------------------------ #
[docs]class Cond(FunctionBlock):
"""
:blockname:`COND`
.. table::
:align: left
+------------+---------+---------+
| inputs | outputs | states |
+------------+---------+---------+
| 1 | 1 | 0 |
+------------+---------+---------+
| A(N,M) | float | |
+------------+---------+---------+
"""
nin = 1
nout = 1
[docs] def __init__(self, **blockargs):
"""
Compute the matrix condition number.
:param blockargs: |BlockOptions|
:type blockargs: dict
:return: A COND block
:rtype: Cond instance
:seealso: :func:`numpy.linalg.cond`
"""
super().__init__(**blockargs)
self.type = 'cond'
def output(self, t=None):
mat = self.inputs[0]
out = np.linalg.cond(mat)
return [mat]
if __name__ == "__main__":
import pathlib
import os.path
exec(open(os.path.join(pathlib.Path(__file__).parent.absolute(), "test_functions.py")).read())