"""
| Additional tools available for programmer
| Author: MaxBQb
"""
from __future__ import annotations
import functools
import itertools
import math
import typing
from win_magnification import types
[docs]def get_matrix_side(elements_count: int, dimension_count=2):
"""
| Gets size of square matrix side
| Example: linear matrix 9x9:
| **elements_count** = 81
| **dimension_count** = 2
:param elements_count: Total count of elements in matrix
:param dimension_count: Array = 1, matrix = 2, cube = 3, etc.
:return: Matrix rows/columns count
"""
return int(math.pow(elements_count, 1.0 / dimension_count))
[docs]def pos_for_matrix(array_len: int, *coords: int) -> int:
"""
| Get index of element in linear structure,
defined by it's coordinates.
| Example:
>>> array = (
... 0, 0, 0,
... 0, 1, 2,
... 0, 0, 0,
... )
>>> array[pos_for_matrix(len(array), 1, 2)]
2
:param array_len: Size fo linear structure
:param coords: Element coordinates
:return: Index in array
"""
row_len = get_matrix_side(array_len, len(coords))
return sum(
row_len ** i * pos for (i, pos) in enumerate(reversed(coords), 0)
)
[docs]def matrix_to_str(matrix: Matrix.Linear):
"""
| Converts matrix to pretty string format
| Example:
>>> matrix_to_str((2.0,))
'2.0'
>>> print(matrix_to_str((1.0,2.0,-3.9,4.0)))
1.0 2.0
-3.9 4.0
:param matrix: Array which sqrt(size) is natural number
"""
row_len = get_matrix_side(len(matrix))
matrix = tuple(str(round(element, 4)) for element in matrix)
negative_column = [False] * row_len
column_width = [1] * row_len
for i in range(row_len):
for j, element in enumerate(matrix[i * row_len:(1 + i) * row_len]):
if element.startswith('-'):
negative_column[j] = True
if len(element) > column_width[j]:
column_width[j] = len(element)
for i in range(row_len):
# Exclude '-' from width calculation
if negative_column[i]:
column_width[i] -= 1
return '\n'.join(
(' '.join((
(' ' if not (j == 0 and not negative_column[j]) and not element.startswith('-')
else '') + element.ljust(column_width[j], ' ')
for j, element in enumerate(matrix[i * row_len:(1 + i) * row_len])
))).rstrip() for i in range(row_len)
)
[docs]def print_matrix(matrix: Matrix.Linear):
"""
| Prints matrix in pretty format
| Example:
>>> print_matrix((2.0,))
2.0
>>> print_matrix((1.0,2.0,-3.9,4.0))
1.0 2.0
-3.9 4.0
:param matrix: Array which sqrt(size) is natural number
"""
print(matrix_to_str(matrix))
[docs]def get_filled_matrix(value=0.0, size=0) -> Matrix.Linear:
return (value,) * size
[docs]def get_simple_color_matrix(
mul_red=1.0, mul_green=1.0, mul_blue=1.0, mul_alpha=1.0,
add_red=0.0, add_green=0.0, add_blue=0.0, add_alpha=0.0,
) -> types.ColorMatrix:
"""
| Creates simple color transformation matrix
| Example:
>>> print_matrix(get_simple_color_matrix(
... 1.1, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0
... ))
1.1 0.0 0.0 0.0 0.0
0.0 2.0 0.0 0.0 0.0
0.0 0.0 3.0 0.0 0.0
0.0 0.0 0.0 4.0 0.0
5.0 6.0 7.0 8.0 1.0
:param mul_red: Red color multiplier
:param mul_green: Green color multiplier
:param mul_blue: Blue color multiplier
:param mul_alpha: Colors transparency multiplier
:param add_red: Red color addendum
:param add_green: Green color addendum
:param add_blue: Blue color addendum
:param add_alpha: Colors transparency addendum
:return: Color transformation matrix
"""
return (
mul_red, 0.0, 0.0, 0.0, 0.0,
0.0, mul_green, 0.0, 0.0, 0.0,
0.0, 0.0, mul_blue, 0.0, 0.0,
0.0, 0.0, 0.0, mul_alpha, 0.0,
add_red, add_green, add_blue, add_alpha, 1.0,
)
Transition: 'typing.TypeAlias' = typing.Callable[[typing.Union[float, int]], 'Matrix.Linear']
"""
Function with predefined transition matrix
makes predefined start moved towards predefined end,
with scale of value, which normally stays between 0 (start) and 1 (end)
to get transition effect
"""
[docs]def get_transition(start: Matrix.Linear, end: Matrix.Linear) -> 'Transition':
"""
| Make function which returns **start** moved towards **end**
| with scale of **value** (param of that function):
| **value** = 0 |=> **start**
| **value** = 1 |=> **end**
| 0 <= **value** <= 1 |=> **start** moved towards **end**
| **value** > 1 |=> **start** moved towards **end** out of **end** bound
| **value** < 0 |=> **end** moved towards **start** out of **start** bound
| Example:
>>> move = get_transition((0, 0, 0), (10, 10, 10))
>>> move(0)
(0.0, 0.0, 0.0)
>>> move(1)
(10.0, 10.0, 10.0)
>>> move(0.4)
(4.0, 4.0, 4.0)
>>> move(-0.4)
(-4.0, -4.0, -4.0)
>>> move(-1.4)
(-14.0, -14.0, -14.0)
:param start: :abbr:`Initial state (transit from)`
:param end: :abbr:`Final state (transit to)`
:return: Transition function from **start** to **end** matrix
"""
diff = tuple(
end[i] - start[i] for i in range(len(start))
)
def transit(value: typing.Union[float, int] = 1.0) -> Matrix.Linear:
"""
Make tuple start moved towards tuple end
with scale of value
:param value: Float scale of transition
normally stays between :abbr:`0 (start)` and :abbr:`1 (end)` to get transition effect
:return: Start matrix moved towards end matrix with value scale
"""
value = float(value)
return tuple(
start[i] + diff[i] * value for i in range(len(start))
)
return transit
[docs]def combine_matrices(first: Matrix.Linear, second: Matrix.Linear):
"""
| Multiplies matrices, can be used to combine color transformations
| Example:
>>> A = (1, 2, 3, 4)
>>> B = (5, 6, 7, 8)
>>> combine_matrices(A, B)
(19, 22, 43, 50)
>>> combine_matrices(A, (1,))
Traceback (most recent call last):
...
ValueError: Matrices must be the same size!
:param first: Matrix A
:param second: Matrix B
:return: A*B
:raises ValueError: On matrices sizes mismatch
"""
if len(first) != len(second):
raise ValueError("Matrices must be the same size!")
row_len = get_matrix_side(len(first))
return tuple(itertools.chain(*([sum(
a * b for a, b in zip(
first[i * row_len:(1 + i) * row_len],
second[j::row_len]
)) for j in range(row_len)
] for i in range(row_len)
)))
[docs]def replace(func: typing.Callable) -> typing.Callable:
"""
| Decorator, replaces one function with another (**func**)
| Example:
>>> def call():
... print("call")
...
>>> @replace(call)
... def talk():
... '''talk docs'''
... print("talk")
>>> talk()
call
>>> talk.__doc__
'talk docs'
:param func: Function which will replace any other
:return: Wrapper, that acts like **func**
"""
def wrapper(inner_func: typing.Callable) -> typing.Callable:
return functools.wraps(inner_func)(func)
return wrapper
[docs]class Matrix:
"""
:abbr:`Matrix (from linear algebra)` wrapper
.. automethod:: __str__
.. automethod:: __matmul__
.. automethod:: __mul__
.. automethod:: __add__
.. automethod:: __sub__
.. automethod:: __neg__
"""
Linear: 'typing.TypeAlias' = typing.Tuple[float, ...]
"""
:abbr:`Matrix (linear algebra term)` represented as flat tuple of floats
"""
Square: 'typing.TypeAlias' = typing.Tuple[typing.Tuple[float, ...], ...]
"""
:abbr:`Matrix (linear algebra term)` represented as tuple of tuples of floats
"""
LinearLike: 'typing.TypeAlias' = typing.Collection[typing.Union[float, int]]
"""
:abbr:`Matrix (linear algebra term)` represented as collection of numbers
"""
SquareLike: 'typing.TypeAlias' = typing.Collection[typing.Collection[typing.Union[float, int]]]
"""
:abbr:`Matrix (linear algebra term)` represented as collection of collections of numbers
"""
Any: 'typing.TypeAlias' = typing.Union[
LinearLike,
SquareLike,
'Matrix',
]
"""
Any :abbr:`matrix (linear algebra term)`: :attr:`SquareLike`, :attr:`LinearLike` or :class:`Matrix` itself
"""
_ACCURACY = 8+1
'''All elements round precision'''
def __init__(self):
self._value: 'Matrix.Linear' = tuple()
self._size = 0
self._side_size = 0
def _resize(self, value: int):
self._size = value
side = get_matrix_side(self._size)
self._side_size = side
side **= 2
if side != value:
raise ValueError("Matrix size must have natural square root!\n"
f"Expect at least {side}, got {value}")
self._value = get_filled_matrix()
@property
def linear(self) -> Linear:
"""
| Get/set linear matrix from/to raw value
| Example:
>>> matrix = Matrix.from_linear((1,2,3,4))
>>> matrix.linear = [4, 5, 6.9, 7]
>>> matrix.linear
(4.0, 5.0, 6.9, 7.0)
>>> matrix.linear = [0]*5
Traceback (most recent call last):
...
ValueError: Linear matrix size mismatch
Expected 4, got 5
| |Accessors: Get Set|
:raises ValueError: On size of new linear matrix musmatch the old one
"""
return self._value
@linear.setter
def linear(self, value: LinearLike) -> None:
if len(value) != self._size:
raise ValueError(f"Linear matrix size mismatch\n"
f"Expected {self._size}, got {len(value)}")
accuracy = self._ACCURACY
self._value = tuple(
round(float(element), accuracy) for element in value
)
@property
def square(self) -> Square:
"""
| Get/set :abbr:`square matrix (array of arrays)` from/to raw value
| Example:
>>> matrix = Matrix.from_linear((1,2,3,4))
>>> matrix.square = [
... [4, 5],
... [6.9, 7]
... ]
>>> matrix.linear
(4.0, 5.0, 6.9, 7.0)
>>> matrix.square
((4.0, 5.0), (6.9, 7.0))
>>> matrix.square = [0]*4
Traceback (most recent call last):
...
TypeError: 'int' object is not iterable
| |Accessors: Get Set|
"""
value = self._value
size = self._side_size
return tuple(
value[i * size:(i + 1) * size] for i in range(size)
)
@square.setter
def square(self, value: SquareLike):
self.linear = tuple(itertools.chain(*value))
[docs] def __str__(self):
"""
| Converts matrix to string
| Example:
>>> print(Matrix.from_linear((1.0,2.0,3.0,4.0)))
1.0 2.0
3.0 4.0
"""
return matrix_to_str(self._value)
[docs] def __matmul__(self, other: typing.Union[Any, int, float]):
"""
| Multipies matrices
| Example:
>>> print(Matrix.from_linear((1,2,3,4)) * Matrix.from_linear((5,6,7,8)))
19.0 22.0
43.0 50.0
>>> Matrix.from_linear((1.0,2.0,3.0,4.0)) @ "hello world"
Traceback (most recent call last):
...
TypeError: Can't multiply matrix and 'str'
:raises TypeError: When unable to convert operand
"""
matrix = self.from_any(other)
if matrix is None:
raise TypeError(f"Can't multiply matrix and {type(other).__name__!r}")
return Matrix.from_linear(combine_matrices(self.linear, matrix.linear))
[docs] def __mul__(self, other: typing.Union[Any, int, float]):
"""
| Multiply matrix and number/:meth:`matrix <__matmul__>`
| Example:
>>> print(Matrix.from_linear((1.0,2.0,3.0,4.0)) * 1)
1.0 2.0
3.0 4.0
>>> test_matrix = Matrix.from_linear((1.0,2.0,3.0,4.0))
>>> test_matrix *= -2
>>> print(test_matrix)
-2.0 -4.0
-6.0 -8.0
>>> print(-Matrix.from_linear((1.0,2.0,3.0,4.0)) * 2)
-2.0 -4.0
-6.0 -8.0
>>> print(Matrix.from_linear((1.0,2.0,3.0,4.0)) * Matrix.from_linear((5.0,6.0,7.0,8.0)))
19.0 22.0
43.0 50.0
"""
if isinstance(other, (int, float)):
origin_matrix = self._value
return Matrix.from_linear(tuple(
element * other for element in origin_matrix
))
return self @ other
[docs] def __add__(self, other: typing.Union[Any, int, float]):
"""
| Add matrix/number to matrix
| Example:
>>> print(Matrix.from_linear((1.0,2.0,3.0,4.0)) + Matrix.from_linear((4.1,3.2,2.3,1.4)))
5.1 5.2
5.3 5.4
>>> print(Matrix.from_linear((1.0,2.0,3.0,4.0)) + 4)
5.0 6.0
7.0 8.0
>>> Matrix.from_linear((1.0,2.0,3.0,4.0)) + "hello world"
Traceback (most recent call last):
...
TypeError: Can't add 'str' to matrix
:raises TypeError: When unable to convert operand
"""
if isinstance(other, (int, float)):
other = get_filled_matrix(float(other), self._size)
matrix = self.from_any(other)
if matrix is None:
raise TypeError(f"Can't add {type(other).__name__!r} to matrix")
matrix_a, matrix_b = self.linear, matrix.linear
return Matrix.from_linear(tuple(
matrix_a[i] + matrix_b[i] for i in range(len(matrix_b))
))
[docs] def __sub__(self, other: typing.Union[Any, int, float]):
"""
| Subtract matrix/number from matrix
| Example:
>>> print(Matrix.from_linear((1.1,2.2,3.3,4.4)) - Matrix.from_linear((1.0,2.0,3.0,4.0)))
0.1 0.2
0.3 0.4
>>> print(Matrix.from_linear((1.0,2.0,3.0,4.0)) - 1)
0.0 1.0
2.0 3.0
>>> Matrix.from_linear((1.0,2.0,3.0,4.0)) - "hello world"
Traceback (most recent call last):
...
TypeError: Can't subtract 'str' from matrix
:raises TypeError: When unable to convert operand
"""
if isinstance(other, (int, float)):
other = get_filled_matrix(float(other), self._size)
matrix = self.from_any(other)
if matrix is None:
raise TypeError(f"Can't subtract {type(other).__name__!r} from matrix")
matrix_a, matrix_b = self.linear, matrix.linear
return Matrix.from_linear(tuple(
matrix_a[i] - matrix_b[i] for i in range(len(matrix_b))
))
[docs] def __neg__(self):
"""
| :meth:`Multiply <__mul__>` matrix with -1
| Example:
>>> print(-Matrix.from_linear((1.0,2.0,3.0,4.0)))
-1.0 -2.0
-3.0 -4.0
"""
return self * -1
[docs] @classmethod
def from_any(cls, value: Any) -> typing.Optional[Matrix]:
"""
| Convert :abbr:`Linear (flat tuple)`/:abbr:`Square (tuple of tuples)` matrix
to :class:`Matrix`
| Example:
>>> Matrix.from_any((1,2,3.5))
>>> print(Matrix.from_any((1,2,3.5,4)))
1.0 2.0
3.5 4.0
>>> Matrix.from_any(((1,2),(3,)))
>>> print(Matrix.from_any(((1,2),(3,4))))
1.0 2.0
3.0 4.0
>>> print(Matrix.from_any(Matrix.from_any(((1,2),(3,4)))))
1.0 2.0
3.0 4.0
:param value: Source of raw data
:type value: :attr:`Matrix.Any`
:return: Filled matrix, or None on conversion fails
"""
if isinstance(value, Matrix):
return value
matrix = cls.from_square(value)
if matrix is None:
matrix = cls.from_linear(value)
return matrix
[docs] @classmethod
def from_linear(cls, value: LinearLike) -> typing.Optional[Matrix]:
"""
| Convert :abbr:`Linear (flat tuple)` matrix to :class:`Matrix`
| Example:
>>> print(Matrix.from_linear((1,2,3.5,4)))
1.0 2.0
3.5 4.0
>>> Matrix.from_linear(((1,2),(3,4)))
>>> Matrix.from_linear((1, 2))
:param value: Source of raw data
:return: Filled matrix, or None on conversion fails
"""
try:
size = len(value)
matrix = cls()
matrix._resize(size)
matrix.linear = value
except (TypeError, ValueError):
return None
return matrix
[docs] @classmethod
def from_square(cls, value: SquareLike) -> typing.Optional[Matrix]:
"""
| Convert :abbr:`Square (tuple of tuples)` matrix
to :class:`Matrix`
| Example:
>>> print(Matrix.from_square(((1,2),(3,4))))
1.0 2.0
3.0 4.0
>>> Matrix.from_square(((1,2),(3,)))
:param value: Source of raw data
:return: Filled matrix, or None on conversion fails
"""
try:
size = len(value)
matrix = cls()
matrix._resize(size ** 2)
matrix.square = value
except (TypeError, ValueError):
return None
return matrix