Source code for matrices._matrix

"""Mutable 2-dimensional matrix type."""
from __future__ import annotations
from typing import Any, Callable, Self, Sequence, overload, cast, Literal

from ._base import MatrixABC
from ._types import T, V, RowColT, IndexT, NotGiven, Sentinel


[docs]class Matrix(MatrixABC[T]): """Mutable 2-dimensional Matrix type. Matrix objects can be instantiated in several different ways: .. py:function:: Matrix(data: MatrixT[T] [, shape: tuple[int, int], *, default: T]) -> Matrix[T] Initialises a new :class:`Matrix` from another :class:`MatrixT` instance (i.e. another :class:`Matrix` or :class:`FrozenMatrix`). The *shape* and *default* arguments are optional -- if omitted the values from the :class:`MatrixT` passed as *data* will be used. :Example: >>> m = Matrix([1, 2, 3, 4], shape=(2, 2), default=0) >>> print(m) 0 1 ┌ ┐ 0 │ 1 2 │ 1 │ 3 4 │ └ ┘ >>> n = Matrix(m, shape=(3, 3)) # default is inferred from m >>> print(n) 0 1 2 ┌ ┐ 0 │ 1 2 0 │ 1 │ 3 4 0 │ 2 │ 0 0 0 │ └ ┘ .. py:function:: Matrix(data: Sequence[Sequence[T]] [, shape: tuple[int, int]], *, default: T) -> Matrix[T] Initialises a new :class:`Matrix` from a sequence of sequences (e.g. a list of lists, or a tuple of tuples). The *shape* argument is optional. If provided *data* will be reshaped to conform to the specified *shape*, otherwise the *shape* will be inferred as follows: The row count equals the length of the outer sequence, the column count equals the length of the first subsequence. The *default* argument is required. :Example: >>> m = Matrix([[1, 2, 3], [4, 5, 6]], default=0) >>> print(m) 0 1 2 ┌ ┐ 0 │ 1 2 3 │ 1 │ 4 5 6 │ └ ┘ >>> m = Matrix(((1, 2), ), shape=(2, 2), default=0) >>> print(m) 0 1 ┌ ┐ 0 │ 1 2 │ 1 │ 0 0 │ └ ┘ .. py:function:: Matrix(data: Sequence[T], shape: tuple[int, int], *, default: T) -> Matrix[T] Initialises a new :class:`Matrix` from a "flat" sequence. The *shape* and *default* arguments are both required, because neither of them can be inferred from *data*. The values from *data* will be read row-wise, and padded with *default* if they run out before the entire :class:`Matrix` is filled. Leftover values in *data* will simply be disregarded. :Example: >>> m = Matrix([1, 2, 3, 4, 5], shape=(2, 3), default=0) >>> print(m) 0 1 2 ┌ ┐ 0 │ 1 2 3 │ 1 │ 4 5 0 │ └ ┘ >>> m = Matrix(range(1, 100), shape=(2, 5), default=0) >>> print(m) 0 1 2 3 4 ┌ ┐ 0 │ 1 2 3 4 5 │ 1 │ 6 7 8 9 10 │ └ ┘ """ # SHAPE MANIPULATION
[docs] def transpose(self) -> Self: self._transpose() return self
[docs] def resize(self, rows: int | tuple[int, int], cols: int | None = None) -> Self: self._resize(rows, cols) return self
[docs] def flip(self, *, by: RowColT = "row") -> Self: self._flip(by=by) return self
[docs] def insertrow(self, index: int, data: Sequence[T]) -> Self: self._insertrow(index, data) return self
[docs] def insertcol(self, index: int, data: Sequence[T]) -> Self: self._insertcol(index, data) return self
[docs] def removerow(self, index: int) -> Self: self._removerow(index) return self
[docs] def removecol(self, index: int) -> Self: self._removecol(index) return self
[docs] def swaprows(self, a_index: int, b_index: int) -> Self: self._swaprows(a_index, b_index) return self
[docs] def swapcols(self, a_index: int, b_index: int) -> Self: self._swapcols(a_index, b_index) return self
# MATRIX OPERATIONS
[docs] def imatadd(self, other: MatrixABC[V]) -> Self | Matrix[V]: """Add the matrix *other* in-situ. This behaves similar to :func:`matadd()`, but applies the result to the :class:`Matrix` instance itself. Equivalent to using the :code:`+=` operator with another :class:`MatrixT` as the right operand. :Example: >>> m = Matrix([[1, 1, 1], [2, 2, 2]], default=0) >>> m.imatadd(m) >>> print(m) 0 1 2 ┌ ┐ 0 │ 2 2 2 │ 1 │ 4 4 4 │ └ ┘ :param other: The :class:`MatrixT` instance whose cell values should be added to the cell values of this matrix. :returns: Its own :class:`Matrix` instance. """ self._imatadd(other) return self
[docs] def imatsub(self, other: MatrixABC[V]) -> Self | Matrix[V]: """Substitute the matrix *other* in-situ. This behaves similar to :func:`matsub()`, but applies the result to the :class:`Matrix` instance itself. Equivalent to using the :code:`-=` operator with another :class:`MatrixT` as the right operand. :Example: >>> m = Matrix([[1, 1, 1], [2, 2, 2]], default=0) >>> m.imatsub(m) >>> print(m) 0 1 2 ┌ ┐ 0 │ 0 0 0 │ 1 │ 0 0 0 │ └ ┘ :param other: The :class:`MatrixT` instance whose cell values should be subtracted from the cell values of this matrix. :returns: Its own :class:`Matrix` instance. """ self._imatsub(other) return self
[docs] def imatmul(self, other: MatrixABC[V]) -> Self | Matrix[V]: """Multiply with the matrix *other* in-situ. This behaves similar to :func:`matmul()`, but applies the result to the :class:`Matrix` instance itself. Note that this will alter the shape of the matrix. The matrix passed as *other* must have the inverse shape of the present matrix. E.g. if the instance has shape (2, 3), then the *other* matrix must have the shape (3, 2), otherwise the dot product is undefined and a `ValueError` will be raised. Equivalent to using the :code:`@=` operator with another :class:`MatrixT` as the right operand. :Example: >>> m = Matrix([[1, 1, 1], [2, 2, 2]], default=0) >>> n = Matrix([[1, 2], [1, 2], [1, 2]], default=0) >>> m.imatmul(m) >>> print(m) 0 1 ┌ ┐ 0 │ 3 6 │ 1 │ 6 12 │ └ ┘ :param other: The :class:`MatrixT` instance whose cell values should be added to the cell values of this matrix. :returns: Its own :class:`Matrix` instance. :raises ValueError: if the shape of *other* is not the inverse of this matrix instance itself. """ self._imatmul(other) return self
[docs] def iscaladd(self, scalar: V) -> Self | Matrix[V]: self._iscaladd(scalar) return self
[docs] def iscalsub(self, scalar: V) -> Self | Matrix[V]: self._iscalsub(scalar) return self
[docs] def iscalmul(self, scalar: V) -> Self | Matrix[V]: self._iscalmul(scalar) return self
[docs] def map( self, func: Callable[..., V], *args: Any, **kwargs: Any ) -> Self | Matrix[V]: self._map(func, *args, **kwargs) return self
# PROPERTIES @property def shape(self) -> tuple[int, int]: return self._shape @shape.setter def shape(self, shape: tuple[int, int]) -> None: self.resize(shape) @property def default(self) -> T: return self._default @default.setter def default(self, default: T) -> None: self._default = default # DATA GETTERS AND SETTERS @overload def set( self, _key: tuple[IndexT, IndexT], _values: T | Sequence[T] | Sequence[Sequence[T] | MatrixABC[T]], /, ) -> Self: ... @overload def set( self, _row: IndexT, _col: IndexT, _values: T | Sequence[T] | Sequence[Sequence[T] | MatrixABC[T]], /, ) -> Self: ...
[docs] def set( self, arg1: IndexT | tuple[IndexT, IndexT], arg2: IndexT | T | Sequence[T] | Sequence[Sequence[T] | MatrixABC[T]] | Sentinel = NotGiven, arg3: T | Sequence[T] | Sequence[Sequence[T] | MatrixABC[T]] | Sentinel = NotGiven, /, ) -> Self: """ (TO BE WRITTEN) """ if arg3 is NotGiven: if not isinstance(arg1, Sequence): raise TypeError( "Argument '_key' must be of type tuple[int | slice | tuple[int, ...], " f"int | slice | tuple[int, ...]], not {type(arg1)}" ) if len(arg1) != 2: raise ValueError( "Argument '_key' must be exactly of length 2, got sequence of length " f"{len(arg1)}" ) if arg2 is NotGiven: raise TypeError("Missing required positional argument '_values'") row, col = arg1 values = arg2 else: if arg2 is NotGiven: raise TypeError("Missing required positional argument '_col'") if (not isinstance(arg1, (int, slice)) and not (isinstance(arg1, tuple) and len(arg1) > 0 and all(isinstance(i, int) for i in arg1))): raise TypeError( "Argument '_row' must be of type int | slice | tuple[int, ...], " f"not {type(arg1)}" ) if (not isinstance(arg2, (int, slice)) and not (isinstance(arg2, tuple) and len(arg2) > 0 and all(isinstance(i, int) for i in arg2))): raise TypeError( "Argument '_col' must be of type int | slice | tuple[int, ...], " f"not {type(arg2)}" ) if arg3 is NotGiven: raise TypeError("Missing required positional argument '_values'") row = arg1 # type: ignore # @FIXME col = arg2 # type: ignore # @FIXME values = arg3 if not isinstance(row, (slice, int, tuple)) or not isinstance(col, (slice, int, tuple)): raise TypeError( "Matrix indices must be tuples of type (int | slice | tuple[int, ...], int | slice " f"| tuple[int, ...]), not ({type(row)}, {type(col)})" ) if isinstance(row, (slice, tuple)) or isinstance(col, (slice, tuple)): if not isinstance(values, (Sequence, MatrixABC)): raise TypeError( "Values passed for matrix slice assignment must be of type Sequence or " f"MatrixABC, not {type(values)}" ) values_ = cast(Sequence[T] | Sequence[Sequence[T]] | MatrixABC[T], values) self._setslice(row, col, values_) else: value_ = cast(T, values) self._setitem(row, col, value_) return self
def __setitem__(self, key: tuple[IndexT, IndexT], value: T | Sequence[T] | Sequence[Sequence[T] | MatrixABC[T]]) -> None: self.set(key, value) def __delitem__(self, key: tuple[IndexT, IndexT]) -> None: self.__setitem__(key, self._default) # DUNDER METHODS def __iadd__(self, other: MatrixABC[V] | V) -> Self | Matrix[V]: if isinstance(other, MatrixABC): return self.imatadd(other) return self.iscaladd(other) def __isub__(self, other: MatrixABC[V] | V) -> Self | Matrix[V]: if isinstance(other, MatrixABC): return self.imatsub(other) return self.iscalsub(other) def __imul__(self, other: V) -> Self | Matrix[V]: return self.iscalmul(other) def __imatmul__(self, other: MatrixABC[V]) -> Self | Matrix[V]: return self.imatmul(other)