1242 lines
35 KiB
Python
1242 lines
35 KiB
Python
"""
|
|
|
|
Module for the SDM class.
|
|
|
|
"""
|
|
|
|
from operator import add, neg, pos, sub, mul
|
|
from collections import defaultdict
|
|
|
|
from sympy.utilities.iterables import _strongly_connected_components
|
|
|
|
from .exceptions import DMBadInputError, DMDomainError, DMShapeError
|
|
|
|
from .ddm import DDM
|
|
from .lll import ddm_lll, ddm_lll_transform
|
|
from sympy.polys.domains import QQ
|
|
|
|
|
|
class SDM(dict):
|
|
r"""Sparse matrix based on polys domain elements
|
|
|
|
This is a dict subclass and is a wrapper for a dict of dicts that supports
|
|
basic matrix arithmetic +, -, *, **.
|
|
|
|
|
|
In order to create a new :py:class:`~.SDM`, a dict
|
|
of dicts mapping non-zero elements to their
|
|
corresponding row and column in the matrix is needed.
|
|
|
|
We also need to specify the shape and :py:class:`~.Domain`
|
|
of our :py:class:`~.SDM` object.
|
|
|
|
We declare a 2x2 :py:class:`~.SDM` matrix belonging
|
|
to QQ domain as shown below.
|
|
The 2x2 Matrix in the example is
|
|
|
|
.. math::
|
|
A = \left[\begin{array}{ccc}
|
|
0 & \frac{1}{2} \\
|
|
0 & 0 \end{array} \right]
|
|
|
|
|
|
>>> from sympy.polys.matrices.sdm import SDM
|
|
>>> from sympy import QQ
|
|
>>> elemsdict = {0:{1:QQ(1, 2)}}
|
|
>>> A = SDM(elemsdict, (2, 2), QQ)
|
|
>>> A
|
|
{0: {1: 1/2}}
|
|
|
|
We can manipulate :py:class:`~.SDM` the same way
|
|
as a Matrix class
|
|
|
|
>>> from sympy import ZZ
|
|
>>> A = SDM({0:{1: ZZ(2)}, 1:{0:ZZ(1)}}, (2, 2), ZZ)
|
|
>>> B = SDM({0:{0: ZZ(3)}, 1:{1:ZZ(4)}}, (2, 2), ZZ)
|
|
>>> A + B
|
|
{0: {0: 3, 1: 2}, 1: {0: 1, 1: 4}}
|
|
|
|
Multiplication
|
|
|
|
>>> A*B
|
|
{0: {1: 8}, 1: {0: 3}}
|
|
>>> A*ZZ(2)
|
|
{0: {1: 4}, 1: {0: 2}}
|
|
|
|
"""
|
|
|
|
fmt = 'sparse'
|
|
|
|
def __init__(self, elemsdict, shape, domain):
|
|
super().__init__(elemsdict)
|
|
self.shape = self.rows, self.cols = m, n = shape
|
|
self.domain = domain
|
|
|
|
if not all(0 <= r < m for r in self):
|
|
raise DMBadInputError("Row out of range")
|
|
if not all(0 <= c < n for row in self.values() for c in row):
|
|
raise DMBadInputError("Column out of range")
|
|
|
|
def getitem(self, i, j):
|
|
try:
|
|
return self[i][j]
|
|
except KeyError:
|
|
m, n = self.shape
|
|
if -m <= i < m and -n <= j < n:
|
|
try:
|
|
return self[i % m][j % n]
|
|
except KeyError:
|
|
return self.domain.zero
|
|
else:
|
|
raise IndexError("index out of range")
|
|
|
|
def setitem(self, i, j, value):
|
|
m, n = self.shape
|
|
if not (-m <= i < m and -n <= j < n):
|
|
raise IndexError("index out of range")
|
|
i, j = i % m, j % n
|
|
if value:
|
|
try:
|
|
self[i][j] = value
|
|
except KeyError:
|
|
self[i] = {j: value}
|
|
else:
|
|
rowi = self.get(i, None)
|
|
if rowi is not None:
|
|
try:
|
|
del rowi[j]
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
if not rowi:
|
|
del self[i]
|
|
|
|
def extract_slice(self, slice1, slice2):
|
|
m, n = self.shape
|
|
ri = range(m)[slice1]
|
|
ci = range(n)[slice2]
|
|
|
|
sdm = {}
|
|
for i, row in self.items():
|
|
if i in ri:
|
|
row = {ci.index(j): e for j, e in row.items() if j in ci}
|
|
if row:
|
|
sdm[ri.index(i)] = row
|
|
|
|
return self.new(sdm, (len(ri), len(ci)), self.domain)
|
|
|
|
def extract(self, rows, cols):
|
|
if not (self and rows and cols):
|
|
return self.zeros((len(rows), len(cols)), self.domain)
|
|
|
|
m, n = self.shape
|
|
if not (-m <= min(rows) <= max(rows) < m):
|
|
raise IndexError('Row index out of range')
|
|
if not (-n <= min(cols) <= max(cols) < n):
|
|
raise IndexError('Column index out of range')
|
|
|
|
# rows and cols can contain duplicates e.g. M[[1, 2, 2], [0, 1]]
|
|
# Build a map from row/col in self to list of rows/cols in output
|
|
rowmap = defaultdict(list)
|
|
colmap = defaultdict(list)
|
|
for i2, i1 in enumerate(rows):
|
|
rowmap[i1 % m].append(i2)
|
|
for j2, j1 in enumerate(cols):
|
|
colmap[j1 % n].append(j2)
|
|
|
|
# Used to efficiently skip zero rows/cols
|
|
rowset = set(rowmap)
|
|
colset = set(colmap)
|
|
|
|
sdm1 = self
|
|
sdm2 = {}
|
|
for i1 in rowset & set(sdm1):
|
|
row1 = sdm1[i1]
|
|
row2 = {}
|
|
for j1 in colset & set(row1):
|
|
row1_j1 = row1[j1]
|
|
for j2 in colmap[j1]:
|
|
row2[j2] = row1_j1
|
|
if row2:
|
|
for i2 in rowmap[i1]:
|
|
sdm2[i2] = row2.copy()
|
|
|
|
return self.new(sdm2, (len(rows), len(cols)), self.domain)
|
|
|
|
def __str__(self):
|
|
rowsstr = []
|
|
for i, row in self.items():
|
|
elemsstr = ', '.join('%s: %s' % (j, elem) for j, elem in row.items())
|
|
rowsstr.append('%s: {%s}' % (i, elemsstr))
|
|
return '{%s}' % ', '.join(rowsstr)
|
|
|
|
def __repr__(self):
|
|
cls = type(self).__name__
|
|
rows = dict.__repr__(self)
|
|
return '%s(%s, %s, %s)' % (cls, rows, self.shape, self.domain)
|
|
|
|
@classmethod
|
|
def new(cls, sdm, shape, domain):
|
|
"""
|
|
|
|
Parameters
|
|
==========
|
|
|
|
sdm: A dict of dicts for non-zero elements in SDM
|
|
shape: tuple representing dimension of SDM
|
|
domain: Represents :py:class:`~.Domain` of SDM
|
|
|
|
Returns
|
|
=======
|
|
|
|
An :py:class:`~.SDM` object
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy.polys.matrices.sdm import SDM
|
|
>>> from sympy import QQ
|
|
>>> elemsdict = {0:{1: QQ(2)}}
|
|
>>> A = SDM.new(elemsdict, (2, 2), QQ)
|
|
>>> A
|
|
{0: {1: 2}}
|
|
|
|
"""
|
|
return cls(sdm, shape, domain)
|
|
|
|
def copy(A):
|
|
"""
|
|
Returns the copy of a :py:class:`~.SDM` object
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy.polys.matrices.sdm import SDM
|
|
>>> from sympy import QQ
|
|
>>> elemsdict = {0:{1:QQ(2)}, 1:{}}
|
|
>>> A = SDM(elemsdict, (2, 2), QQ)
|
|
>>> B = A.copy()
|
|
>>> B
|
|
{0: {1: 2}, 1: {}}
|
|
|
|
"""
|
|
Ac = {i: Ai.copy() for i, Ai in A.items()}
|
|
return A.new(Ac, A.shape, A.domain)
|
|
|
|
@classmethod
|
|
def from_list(cls, ddm, shape, domain):
|
|
"""
|
|
|
|
Parameters
|
|
==========
|
|
|
|
ddm:
|
|
list of lists containing domain elements
|
|
shape:
|
|
Dimensions of :py:class:`~.SDM` matrix
|
|
domain:
|
|
Represents :py:class:`~.Domain` of :py:class:`~.SDM` object
|
|
|
|
Returns
|
|
=======
|
|
|
|
:py:class:`~.SDM` containing elements of ddm
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy.polys.matrices.sdm import SDM
|
|
>>> from sympy import QQ
|
|
>>> ddm = [[QQ(1, 2), QQ(0)], [QQ(0), QQ(3, 4)]]
|
|
>>> A = SDM.from_list(ddm, (2, 2), QQ)
|
|
>>> A
|
|
{0: {0: 1/2}, 1: {1: 3/4}}
|
|
|
|
"""
|
|
|
|
m, n = shape
|
|
if not (len(ddm) == m and all(len(row) == n for row in ddm)):
|
|
raise DMBadInputError("Inconsistent row-list/shape")
|
|
getrow = lambda i: {j:ddm[i][j] for j in range(n) if ddm[i][j]}
|
|
irows = ((i, getrow(i)) for i in range(m))
|
|
sdm = {i: row for i, row in irows if row}
|
|
return cls(sdm, shape, domain)
|
|
|
|
@classmethod
|
|
def from_ddm(cls, ddm):
|
|
"""
|
|
converts object of :py:class:`~.DDM` to
|
|
:py:class:`~.SDM`
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy.polys.matrices.ddm import DDM
|
|
>>> from sympy.polys.matrices.sdm import SDM
|
|
>>> from sympy import QQ
|
|
>>> ddm = DDM( [[QQ(1, 2), 0], [0, QQ(3, 4)]], (2, 2), QQ)
|
|
>>> A = SDM.from_ddm(ddm)
|
|
>>> A
|
|
{0: {0: 1/2}, 1: {1: 3/4}}
|
|
|
|
"""
|
|
return cls.from_list(ddm, ddm.shape, ddm.domain)
|
|
|
|
def to_list(M):
|
|
"""
|
|
|
|
Converts a :py:class:`~.SDM` object to a list
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy.polys.matrices.sdm import SDM
|
|
>>> from sympy import QQ
|
|
>>> elemsdict = {0:{1:QQ(2)}, 1:{}}
|
|
>>> A = SDM(elemsdict, (2, 2), QQ)
|
|
>>> A.to_list()
|
|
[[0, 2], [0, 0]]
|
|
|
|
"""
|
|
m, n = M.shape
|
|
zero = M.domain.zero
|
|
ddm = [[zero] * n for _ in range(m)]
|
|
for i, row in M.items():
|
|
for j, e in row.items():
|
|
ddm[i][j] = e
|
|
return ddm
|
|
|
|
def to_list_flat(M):
|
|
m, n = M.shape
|
|
zero = M.domain.zero
|
|
flat = [zero] * (m * n)
|
|
for i, row in M.items():
|
|
for j, e in row.items():
|
|
flat[i*n + j] = e
|
|
return flat
|
|
|
|
def to_dok(M):
|
|
return {(i, j): e for i, row in M.items() for j, e in row.items()}
|
|
|
|
def to_ddm(M):
|
|
"""
|
|
Convert a :py:class:`~.SDM` object to a :py:class:`~.DDM` object
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy.polys.matrices.sdm import SDM
|
|
>>> from sympy import QQ
|
|
>>> A = SDM({0:{1:QQ(2)}, 1:{}}, (2, 2), QQ)
|
|
>>> A.to_ddm()
|
|
[[0, 2], [0, 0]]
|
|
|
|
"""
|
|
return DDM(M.to_list(), M.shape, M.domain)
|
|
|
|
def to_sdm(M):
|
|
return M
|
|
|
|
@classmethod
|
|
def zeros(cls, shape, domain):
|
|
r"""
|
|
|
|
Returns a :py:class:`~.SDM` of size shape,
|
|
belonging to the specified domain
|
|
|
|
In the example below we declare a matrix A where,
|
|
|
|
.. math::
|
|
A := \left[\begin{array}{ccc}
|
|
0 & 0 & 0 \\
|
|
0 & 0 & 0 \end{array} \right]
|
|
|
|
>>> from sympy.polys.matrices.sdm import SDM
|
|
>>> from sympy import QQ
|
|
>>> A = SDM.zeros((2, 3), QQ)
|
|
>>> A
|
|
{}
|
|
|
|
"""
|
|
return cls({}, shape, domain)
|
|
|
|
@classmethod
|
|
def ones(cls, shape, domain):
|
|
one = domain.one
|
|
m, n = shape
|
|
row = dict(zip(range(n), [one]*n))
|
|
sdm = {i: row.copy() for i in range(m)}
|
|
return cls(sdm, shape, domain)
|
|
|
|
@classmethod
|
|
def eye(cls, shape, domain):
|
|
"""
|
|
|
|
Returns a identity :py:class:`~.SDM` matrix of dimensions
|
|
size x size, belonging to the specified domain
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy.polys.matrices.sdm import SDM
|
|
>>> from sympy import QQ
|
|
>>> I = SDM.eye((2, 2), QQ)
|
|
>>> I
|
|
{0: {0: 1}, 1: {1: 1}}
|
|
|
|
"""
|
|
rows, cols = shape
|
|
one = domain.one
|
|
sdm = {i: {i: one} for i in range(min(rows, cols))}
|
|
return cls(sdm, shape, domain)
|
|
|
|
@classmethod
|
|
def diag(cls, diagonal, domain, shape):
|
|
sdm = {i: {i: v} for i, v in enumerate(diagonal) if v}
|
|
return cls(sdm, shape, domain)
|
|
|
|
def transpose(M):
|
|
"""
|
|
|
|
Returns the transpose of a :py:class:`~.SDM` matrix
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy.polys.matrices.sdm import SDM
|
|
>>> from sympy import QQ
|
|
>>> A = SDM({0:{1:QQ(2)}, 1:{}}, (2, 2), QQ)
|
|
>>> A.transpose()
|
|
{1: {0: 2}}
|
|
|
|
"""
|
|
MT = sdm_transpose(M)
|
|
return M.new(MT, M.shape[::-1], M.domain)
|
|
|
|
def __add__(A, B):
|
|
if not isinstance(B, SDM):
|
|
return NotImplemented
|
|
return A.add(B)
|
|
|
|
def __sub__(A, B):
|
|
if not isinstance(B, SDM):
|
|
return NotImplemented
|
|
return A.sub(B)
|
|
|
|
def __neg__(A):
|
|
return A.neg()
|
|
|
|
def __mul__(A, B):
|
|
"""A * B"""
|
|
if isinstance(B, SDM):
|
|
return A.matmul(B)
|
|
elif B in A.domain:
|
|
return A.mul(B)
|
|
else:
|
|
return NotImplemented
|
|
|
|
def __rmul__(a, b):
|
|
if b in a.domain:
|
|
return a.rmul(b)
|
|
else:
|
|
return NotImplemented
|
|
|
|
def matmul(A, B):
|
|
"""
|
|
Performs matrix multiplication of two SDM matrices
|
|
|
|
Parameters
|
|
==========
|
|
|
|
A, B: SDM to multiply
|
|
|
|
Returns
|
|
=======
|
|
|
|
SDM
|
|
SDM after multiplication
|
|
|
|
Raises
|
|
======
|
|
|
|
DomainError
|
|
If domain of A does not match
|
|
with that of B
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import ZZ
|
|
>>> from sympy.polys.matrices.sdm import SDM
|
|
>>> A = SDM({0:{1: ZZ(2)}, 1:{0:ZZ(1)}}, (2, 2), ZZ)
|
|
>>> B = SDM({0:{0:ZZ(2), 1:ZZ(3)}, 1:{0:ZZ(4)}}, (2, 2), ZZ)
|
|
>>> A.matmul(B)
|
|
{0: {0: 8}, 1: {0: 2, 1: 3}}
|
|
|
|
"""
|
|
if A.domain != B.domain:
|
|
raise DMDomainError
|
|
m, n = A.shape
|
|
n2, o = B.shape
|
|
if n != n2:
|
|
raise DMShapeError
|
|
C = sdm_matmul(A, B, A.domain, m, o)
|
|
return A.new(C, (m, o), A.domain)
|
|
|
|
def mul(A, b):
|
|
"""
|
|
Multiplies each element of A with a scalar b
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import ZZ
|
|
>>> from sympy.polys.matrices.sdm import SDM
|
|
>>> A = SDM({0:{1: ZZ(2)}, 1:{0:ZZ(1)}}, (2, 2), ZZ)
|
|
>>> A.mul(ZZ(3))
|
|
{0: {1: 6}, 1: {0: 3}}
|
|
|
|
"""
|
|
Csdm = unop_dict(A, lambda aij: aij*b)
|
|
return A.new(Csdm, A.shape, A.domain)
|
|
|
|
def rmul(A, b):
|
|
Csdm = unop_dict(A, lambda aij: b*aij)
|
|
return A.new(Csdm, A.shape, A.domain)
|
|
|
|
def mul_elementwise(A, B):
|
|
if A.domain != B.domain:
|
|
raise DMDomainError
|
|
if A.shape != B.shape:
|
|
raise DMShapeError
|
|
zero = A.domain.zero
|
|
fzero = lambda e: zero
|
|
Csdm = binop_dict(A, B, mul, fzero, fzero)
|
|
return A.new(Csdm, A.shape, A.domain)
|
|
|
|
def add(A, B):
|
|
"""
|
|
|
|
Adds two :py:class:`~.SDM` matrices
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import ZZ
|
|
>>> from sympy.polys.matrices.sdm import SDM
|
|
>>> A = SDM({0:{1: ZZ(2)}, 1:{0:ZZ(1)}}, (2, 2), ZZ)
|
|
>>> B = SDM({0:{0: ZZ(3)}, 1:{1:ZZ(4)}}, (2, 2), ZZ)
|
|
>>> A.add(B)
|
|
{0: {0: 3, 1: 2}, 1: {0: 1, 1: 4}}
|
|
|
|
"""
|
|
|
|
Csdm = binop_dict(A, B, add, pos, pos)
|
|
return A.new(Csdm, A.shape, A.domain)
|
|
|
|
def sub(A, B):
|
|
"""
|
|
|
|
Subtracts two :py:class:`~.SDM` matrices
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import ZZ
|
|
>>> from sympy.polys.matrices.sdm import SDM
|
|
>>> A = SDM({0:{1: ZZ(2)}, 1:{0:ZZ(1)}}, (2, 2), ZZ)
|
|
>>> B = SDM({0:{0: ZZ(3)}, 1:{1:ZZ(4)}}, (2, 2), ZZ)
|
|
>>> A.sub(B)
|
|
{0: {0: -3, 1: 2}, 1: {0: 1, 1: -4}}
|
|
|
|
"""
|
|
Csdm = binop_dict(A, B, sub, pos, neg)
|
|
return A.new(Csdm, A.shape, A.domain)
|
|
|
|
def neg(A):
|
|
"""
|
|
|
|
Returns the negative of a :py:class:`~.SDM` matrix
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import ZZ
|
|
>>> from sympy.polys.matrices.sdm import SDM
|
|
>>> A = SDM({0:{1: ZZ(2)}, 1:{0:ZZ(1)}}, (2, 2), ZZ)
|
|
>>> A.neg()
|
|
{0: {1: -2}, 1: {0: -1}}
|
|
|
|
"""
|
|
Csdm = unop_dict(A, neg)
|
|
return A.new(Csdm, A.shape, A.domain)
|
|
|
|
def convert_to(A, K):
|
|
"""
|
|
|
|
Converts the :py:class:`~.Domain` of a :py:class:`~.SDM` matrix to K
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import ZZ, QQ
|
|
>>> from sympy.polys.matrices.sdm import SDM
|
|
>>> A = SDM({0:{1: ZZ(2)}, 1:{0:ZZ(1)}}, (2, 2), ZZ)
|
|
>>> A.convert_to(QQ)
|
|
{0: {1: 2}, 1: {0: 1}}
|
|
|
|
"""
|
|
Kold = A.domain
|
|
if K == Kold:
|
|
return A.copy()
|
|
Ak = unop_dict(A, lambda e: K.convert_from(e, Kold))
|
|
return A.new(Ak, A.shape, K)
|
|
|
|
def scc(A):
|
|
"""Strongly connected components of a square matrix *A*.
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import ZZ
|
|
>>> from sympy.polys.matrices.sdm import SDM
|
|
>>> A = SDM({0:{0: ZZ(2)}, 1:{1:ZZ(1)}}, (2, 2), ZZ)
|
|
>>> A.scc()
|
|
[[0], [1]]
|
|
|
|
See also
|
|
========
|
|
|
|
sympy.polys.matrices.domainmatrix.DomainMatrix.scc
|
|
"""
|
|
rows, cols = A.shape
|
|
assert rows == cols
|
|
V = range(rows)
|
|
Emap = {v: list(A.get(v, [])) for v in V}
|
|
return _strongly_connected_components(V, Emap)
|
|
|
|
def rref(A):
|
|
"""
|
|
|
|
Returns reduced-row echelon form and list of pivots for the :py:class:`~.SDM`
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import QQ
|
|
>>> from sympy.polys.matrices.sdm import SDM
|
|
>>> A = SDM({0:{0:QQ(1), 1:QQ(2)}, 1:{0:QQ(2), 1:QQ(4)}}, (2, 2), QQ)
|
|
>>> A.rref()
|
|
({0: {0: 1, 1: 2}}, [0])
|
|
|
|
"""
|
|
B, pivots, _ = sdm_irref(A)
|
|
return A.new(B, A.shape, A.domain), pivots
|
|
|
|
def inv(A):
|
|
"""
|
|
|
|
Returns inverse of a matrix A
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import QQ
|
|
>>> from sympy.polys.matrices.sdm import SDM
|
|
>>> A = SDM({0:{0:QQ(1), 1:QQ(2)}, 1:{0:QQ(3), 1:QQ(4)}}, (2, 2), QQ)
|
|
>>> A.inv()
|
|
{0: {0: -2, 1: 1}, 1: {0: 3/2, 1: -1/2}}
|
|
|
|
"""
|
|
return A.from_ddm(A.to_ddm().inv())
|
|
|
|
def det(A):
|
|
"""
|
|
Returns determinant of A
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import QQ
|
|
>>> from sympy.polys.matrices.sdm import SDM
|
|
>>> A = SDM({0:{0:QQ(1), 1:QQ(2)}, 1:{0:QQ(3), 1:QQ(4)}}, (2, 2), QQ)
|
|
>>> A.det()
|
|
-2
|
|
|
|
"""
|
|
return A.to_ddm().det()
|
|
|
|
def lu(A):
|
|
"""
|
|
|
|
Returns LU decomposition for a matrix A
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import QQ
|
|
>>> from sympy.polys.matrices.sdm import SDM
|
|
>>> A = SDM({0:{0:QQ(1), 1:QQ(2)}, 1:{0:QQ(3), 1:QQ(4)}}, (2, 2), QQ)
|
|
>>> A.lu()
|
|
({0: {0: 1}, 1: {0: 3, 1: 1}}, {0: {0: 1, 1: 2}, 1: {1: -2}}, [])
|
|
|
|
"""
|
|
L, U, swaps = A.to_ddm().lu()
|
|
return A.from_ddm(L), A.from_ddm(U), swaps
|
|
|
|
def lu_solve(A, b):
|
|
"""
|
|
|
|
Uses LU decomposition to solve Ax = b,
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import QQ
|
|
>>> from sympy.polys.matrices.sdm import SDM
|
|
>>> A = SDM({0:{0:QQ(1), 1:QQ(2)}, 1:{0:QQ(3), 1:QQ(4)}}, (2, 2), QQ)
|
|
>>> b = SDM({0:{0:QQ(1)}, 1:{0:QQ(2)}}, (2, 1), QQ)
|
|
>>> A.lu_solve(b)
|
|
{1: {0: 1/2}}
|
|
|
|
"""
|
|
return A.from_ddm(A.to_ddm().lu_solve(b.to_ddm()))
|
|
|
|
def nullspace(A):
|
|
"""
|
|
|
|
Returns nullspace for a :py:class:`~.SDM` matrix A
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import QQ
|
|
>>> from sympy.polys.matrices.sdm import SDM
|
|
>>> A = SDM({0:{0:QQ(1), 1:QQ(2)}, 1:{0: QQ(2), 1: QQ(4)}}, (2, 2), QQ)
|
|
>>> A.nullspace()
|
|
({0: {0: -2, 1: 1}}, [1])
|
|
|
|
"""
|
|
ncols = A.shape[1]
|
|
one = A.domain.one
|
|
B, pivots, nzcols = sdm_irref(A)
|
|
K, nonpivots = sdm_nullspace_from_rref(B, one, ncols, pivots, nzcols)
|
|
K = dict(enumerate(K))
|
|
shape = (len(K), ncols)
|
|
return A.new(K, shape, A.domain), nonpivots
|
|
|
|
def particular(A):
|
|
ncols = A.shape[1]
|
|
B, pivots, nzcols = sdm_irref(A)
|
|
P = sdm_particular_from_rref(B, ncols, pivots)
|
|
rep = {0:P} if P else {}
|
|
return A.new(rep, (1, ncols-1), A.domain)
|
|
|
|
def hstack(A, *B):
|
|
"""Horizontally stacks :py:class:`~.SDM` matrices.
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import ZZ
|
|
>>> from sympy.polys.matrices.sdm import SDM
|
|
|
|
>>> A = SDM({0: {0: ZZ(1), 1: ZZ(2)}, 1: {0: ZZ(3), 1: ZZ(4)}}, (2, 2), ZZ)
|
|
>>> B = SDM({0: {0: ZZ(5), 1: ZZ(6)}, 1: {0: ZZ(7), 1: ZZ(8)}}, (2, 2), ZZ)
|
|
>>> A.hstack(B)
|
|
{0: {0: 1, 1: 2, 2: 5, 3: 6}, 1: {0: 3, 1: 4, 2: 7, 3: 8}}
|
|
|
|
>>> C = SDM({0: {0: ZZ(9), 1: ZZ(10)}, 1: {0: ZZ(11), 1: ZZ(12)}}, (2, 2), ZZ)
|
|
>>> A.hstack(B, C)
|
|
{0: {0: 1, 1: 2, 2: 5, 3: 6, 4: 9, 5: 10}, 1: {0: 3, 1: 4, 2: 7, 3: 8, 4: 11, 5: 12}}
|
|
"""
|
|
Anew = dict(A.copy())
|
|
rows, cols = A.shape
|
|
domain = A.domain
|
|
|
|
for Bk in B:
|
|
Bkrows, Bkcols = Bk.shape
|
|
assert Bkrows == rows
|
|
assert Bk.domain == domain
|
|
|
|
for i, Bki in Bk.items():
|
|
Ai = Anew.get(i, None)
|
|
if Ai is None:
|
|
Anew[i] = Ai = {}
|
|
for j, Bkij in Bki.items():
|
|
Ai[j + cols] = Bkij
|
|
cols += Bkcols
|
|
|
|
return A.new(Anew, (rows, cols), A.domain)
|
|
|
|
def vstack(A, *B):
|
|
"""Vertically stacks :py:class:`~.SDM` matrices.
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import ZZ
|
|
>>> from sympy.polys.matrices.sdm import SDM
|
|
|
|
>>> A = SDM({0: {0: ZZ(1), 1: ZZ(2)}, 1: {0: ZZ(3), 1: ZZ(4)}}, (2, 2), ZZ)
|
|
>>> B = SDM({0: {0: ZZ(5), 1: ZZ(6)}, 1: {0: ZZ(7), 1: ZZ(8)}}, (2, 2), ZZ)
|
|
>>> A.vstack(B)
|
|
{0: {0: 1, 1: 2}, 1: {0: 3, 1: 4}, 2: {0: 5, 1: 6}, 3: {0: 7, 1: 8}}
|
|
|
|
>>> C = SDM({0: {0: ZZ(9), 1: ZZ(10)}, 1: {0: ZZ(11), 1: ZZ(12)}}, (2, 2), ZZ)
|
|
>>> A.vstack(B, C)
|
|
{0: {0: 1, 1: 2}, 1: {0: 3, 1: 4}, 2: {0: 5, 1: 6}, 3: {0: 7, 1: 8}, 4: {0: 9, 1: 10}, 5: {0: 11, 1: 12}}
|
|
"""
|
|
Anew = dict(A.copy())
|
|
rows, cols = A.shape
|
|
domain = A.domain
|
|
|
|
for Bk in B:
|
|
Bkrows, Bkcols = Bk.shape
|
|
assert Bkcols == cols
|
|
assert Bk.domain == domain
|
|
|
|
for i, Bki in Bk.items():
|
|
Anew[i + rows] = Bki
|
|
rows += Bkrows
|
|
|
|
return A.new(Anew, (rows, cols), A.domain)
|
|
|
|
def applyfunc(self, func, domain):
|
|
sdm = {i: {j: func(e) for j, e in row.items()} for i, row in self.items()}
|
|
return self.new(sdm, self.shape, domain)
|
|
|
|
def charpoly(A):
|
|
"""
|
|
Returns the coefficients of the characteristic polynomial
|
|
of the :py:class:`~.SDM` matrix. These elements will be domain elements.
|
|
The domain of the elements will be same as domain of the :py:class:`~.SDM`.
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import QQ, Symbol
|
|
>>> from sympy.polys.matrices.sdm import SDM
|
|
>>> from sympy.polys import Poly
|
|
>>> A = SDM({0:{0:QQ(1), 1:QQ(2)}, 1:{0:QQ(3), 1:QQ(4)}}, (2, 2), QQ)
|
|
>>> A.charpoly()
|
|
[1, -5, -2]
|
|
|
|
We can create a polynomial using the
|
|
coefficients using :py:class:`~.Poly`
|
|
|
|
>>> x = Symbol('x')
|
|
>>> p = Poly(A.charpoly(), x, domain=A.domain)
|
|
>>> p
|
|
Poly(x**2 - 5*x - 2, x, domain='QQ')
|
|
|
|
"""
|
|
return A.to_ddm().charpoly()
|
|
|
|
def is_zero_matrix(self):
|
|
"""
|
|
Says whether this matrix has all zero entries.
|
|
"""
|
|
return not self
|
|
|
|
def is_upper(self):
|
|
"""
|
|
Says whether this matrix is upper-triangular. True can be returned
|
|
even if the matrix is not square.
|
|
"""
|
|
return all(i <= j for i, row in self.items() for j in row)
|
|
|
|
def is_lower(self):
|
|
"""
|
|
Says whether this matrix is lower-triangular. True can be returned
|
|
even if the matrix is not square.
|
|
"""
|
|
return all(i >= j for i, row in self.items() for j in row)
|
|
|
|
def lll(A, delta=QQ(3, 4)):
|
|
return A.from_ddm(ddm_lll(A.to_ddm(), delta=delta))
|
|
|
|
def lll_transform(A, delta=QQ(3, 4)):
|
|
reduced, transform = ddm_lll_transform(A.to_ddm(), delta=delta)
|
|
return A.from_ddm(reduced), A.from_ddm(transform)
|
|
|
|
|
|
def binop_dict(A, B, fab, fa, fb):
|
|
Anz, Bnz = set(A), set(B)
|
|
C = {}
|
|
|
|
for i in Anz & Bnz:
|
|
Ai, Bi = A[i], B[i]
|
|
Ci = {}
|
|
Anzi, Bnzi = set(Ai), set(Bi)
|
|
for j in Anzi & Bnzi:
|
|
Cij = fab(Ai[j], Bi[j])
|
|
if Cij:
|
|
Ci[j] = Cij
|
|
for j in Anzi - Bnzi:
|
|
Cij = fa(Ai[j])
|
|
if Cij:
|
|
Ci[j] = Cij
|
|
for j in Bnzi - Anzi:
|
|
Cij = fb(Bi[j])
|
|
if Cij:
|
|
Ci[j] = Cij
|
|
if Ci:
|
|
C[i] = Ci
|
|
|
|
for i in Anz - Bnz:
|
|
Ai = A[i]
|
|
Ci = {}
|
|
for j, Aij in Ai.items():
|
|
Cij = fa(Aij)
|
|
if Cij:
|
|
Ci[j] = Cij
|
|
if Ci:
|
|
C[i] = Ci
|
|
|
|
for i in Bnz - Anz:
|
|
Bi = B[i]
|
|
Ci = {}
|
|
for j, Bij in Bi.items():
|
|
Cij = fb(Bij)
|
|
if Cij:
|
|
Ci[j] = Cij
|
|
if Ci:
|
|
C[i] = Ci
|
|
|
|
return C
|
|
|
|
|
|
def unop_dict(A, f):
|
|
B = {}
|
|
for i, Ai in A.items():
|
|
Bi = {}
|
|
for j, Aij in Ai.items():
|
|
Bij = f(Aij)
|
|
if Bij:
|
|
Bi[j] = Bij
|
|
if Bi:
|
|
B[i] = Bi
|
|
return B
|
|
|
|
|
|
def sdm_transpose(M):
|
|
MT = {}
|
|
for i, Mi in M.items():
|
|
for j, Mij in Mi.items():
|
|
try:
|
|
MT[j][i] = Mij
|
|
except KeyError:
|
|
MT[j] = {i: Mij}
|
|
return MT
|
|
|
|
|
|
def sdm_matmul(A, B, K, m, o):
|
|
#
|
|
# Should be fast if A and B are very sparse.
|
|
# Consider e.g. A = B = eye(1000).
|
|
#
|
|
# The idea here is that we compute C = A*B in terms of the rows of C and
|
|
# B since the dict of dicts representation naturally stores the matrix as
|
|
# rows. The ith row of C (Ci) is equal to the sum of Aik * Bk where Bk is
|
|
# the kth row of B. The algorithm below loops over each nonzero element
|
|
# Aik of A and if the corresponding row Bj is nonzero then we do
|
|
# Ci += Aik * Bk.
|
|
# To make this more efficient we don't need to loop over all elements Aik.
|
|
# Instead for each row Ai we compute the intersection of the nonzero
|
|
# columns in Ai with the nonzero rows in B. That gives the k such that
|
|
# Aik and Bk are both nonzero. In Python the intersection of two sets
|
|
# of int can be computed very efficiently.
|
|
#
|
|
if K.is_EXRAW:
|
|
return sdm_matmul_exraw(A, B, K, m, o)
|
|
|
|
C = {}
|
|
B_knz = set(B)
|
|
for i, Ai in A.items():
|
|
Ci = {}
|
|
Ai_knz = set(Ai)
|
|
for k in Ai_knz & B_knz:
|
|
Aik = Ai[k]
|
|
for j, Bkj in B[k].items():
|
|
Cij = Ci.get(j, None)
|
|
if Cij is not None:
|
|
Cij = Cij + Aik * Bkj
|
|
if Cij:
|
|
Ci[j] = Cij
|
|
else:
|
|
Ci.pop(j)
|
|
else:
|
|
Cij = Aik * Bkj
|
|
if Cij:
|
|
Ci[j] = Cij
|
|
if Ci:
|
|
C[i] = Ci
|
|
return C
|
|
|
|
|
|
def sdm_matmul_exraw(A, B, K, m, o):
|
|
#
|
|
# Like sdm_matmul above except that:
|
|
#
|
|
# - Handles cases like 0*oo -> nan (sdm_matmul skips multipication by zero)
|
|
# - Uses K.sum (Add(*items)) for efficient addition of Expr
|
|
#
|
|
zero = K.zero
|
|
C = {}
|
|
B_knz = set(B)
|
|
for i, Ai in A.items():
|
|
Ci_list = defaultdict(list)
|
|
Ai_knz = set(Ai)
|
|
|
|
# Nonzero row/column pair
|
|
for k in Ai_knz & B_knz:
|
|
Aik = Ai[k]
|
|
if zero * Aik == zero:
|
|
# This is the main inner loop:
|
|
for j, Bkj in B[k].items():
|
|
Ci_list[j].append(Aik * Bkj)
|
|
else:
|
|
for j in range(o):
|
|
Ci_list[j].append(Aik * B[k].get(j, zero))
|
|
|
|
# Zero row in B, check for infinities in A
|
|
for k in Ai_knz - B_knz:
|
|
zAik = zero * Ai[k]
|
|
if zAik != zero:
|
|
for j in range(o):
|
|
Ci_list[j].append(zAik)
|
|
|
|
# Add terms using K.sum (Add(*terms)) for efficiency
|
|
Ci = {}
|
|
for j, Cij_list in Ci_list.items():
|
|
Cij = K.sum(Cij_list)
|
|
if Cij:
|
|
Ci[j] = Cij
|
|
if Ci:
|
|
C[i] = Ci
|
|
|
|
# Find all infinities in B
|
|
for k, Bk in B.items():
|
|
for j, Bkj in Bk.items():
|
|
if zero * Bkj != zero:
|
|
for i in range(m):
|
|
Aik = A.get(i, {}).get(k, zero)
|
|
# If Aik is not zero then this was handled above
|
|
if Aik == zero:
|
|
Ci = C.get(i, {})
|
|
Cij = Ci.get(j, zero) + Aik * Bkj
|
|
if Cij != zero:
|
|
Ci[j] = Cij
|
|
else: # pragma: no cover
|
|
# Not sure how we could get here but let's raise an
|
|
# exception just in case.
|
|
raise RuntimeError
|
|
C[i] = Ci
|
|
|
|
return C
|
|
|
|
|
|
def sdm_irref(A):
|
|
"""RREF and pivots of a sparse matrix *A*.
|
|
|
|
Compute the reduced row echelon form (RREF) of the matrix *A* and return a
|
|
list of the pivot columns. This routine does not work in place and leaves
|
|
the original matrix *A* unmodified.
|
|
|
|
Examples
|
|
========
|
|
|
|
This routine works with a dict of dicts sparse representation of a matrix:
|
|
|
|
>>> from sympy import QQ
|
|
>>> from sympy.polys.matrices.sdm import sdm_irref
|
|
>>> A = {0: {0: QQ(1), 1: QQ(2)}, 1: {0: QQ(3), 1: QQ(4)}}
|
|
>>> Arref, pivots, _ = sdm_irref(A)
|
|
>>> Arref
|
|
{0: {0: 1}, 1: {1: 1}}
|
|
>>> pivots
|
|
[0, 1]
|
|
|
|
The analogous calculation with :py:class:`~.Matrix` would be
|
|
|
|
>>> from sympy import Matrix
|
|
>>> M = Matrix([[1, 2], [3, 4]])
|
|
>>> Mrref, pivots = M.rref()
|
|
>>> Mrref
|
|
Matrix([
|
|
[1, 0],
|
|
[0, 1]])
|
|
>>> pivots
|
|
(0, 1)
|
|
|
|
Notes
|
|
=====
|
|
|
|
The cost of this algorithm is determined purely by the nonzero elements of
|
|
the matrix. No part of the cost of any step in this algorithm depends on
|
|
the number of rows or columns in the matrix. No step depends even on the
|
|
number of nonzero rows apart from the primary loop over those rows. The
|
|
implementation is much faster than ddm_rref for sparse matrices. In fact
|
|
at the time of writing it is also (slightly) faster than the dense
|
|
implementation even if the input is a fully dense matrix so it seems to be
|
|
faster in all cases.
|
|
|
|
The elements of the matrix should support exact division with ``/``. For
|
|
example elements of any domain that is a field (e.g. ``QQ``) should be
|
|
fine. No attempt is made to handle inexact arithmetic.
|
|
|
|
"""
|
|
#
|
|
# Any zeros in the matrix are not stored at all so an element is zero if
|
|
# its row dict has no index at that key. A row is entirely zero if its
|
|
# row index is not in the outer dict. Since rref reorders the rows and
|
|
# removes zero rows we can completely discard the row indices. The first
|
|
# step then copies the row dicts into a list sorted by the index of the
|
|
# first nonzero column in each row.
|
|
#
|
|
# The algorithm then processes each row Ai one at a time. Previously seen
|
|
# rows are used to cancel their pivot columns from Ai. Then a pivot from
|
|
# Ai is chosen and is cancelled from all previously seen rows. At this
|
|
# point Ai joins the previously seen rows. Once all rows are seen all
|
|
# elimination has occurred and the rows are sorted by pivot column index.
|
|
#
|
|
# The previously seen rows are stored in two separate groups. The reduced
|
|
# group consists of all rows that have been reduced to a single nonzero
|
|
# element (the pivot). There is no need to attempt any further reduction
|
|
# with these. Rows that still have other nonzeros need to be considered
|
|
# when Ai is cancelled from the previously seen rows.
|
|
#
|
|
# A dict nonzerocolumns is used to map from a column index to a set of
|
|
# previously seen rows that still have a nonzero element in that column.
|
|
# This means that we can cancel the pivot from Ai into the previously seen
|
|
# rows without needing to loop over each row that might have a zero in
|
|
# that column.
|
|
#
|
|
|
|
# Row dicts sorted by index of first nonzero column
|
|
# (Maybe sorting is not needed/useful.)
|
|
Arows = sorted((Ai.copy() for Ai in A.values()), key=min)
|
|
|
|
# Each processed row has an associated pivot column.
|
|
# pivot_row_map maps from the pivot column index to the row dict.
|
|
# This means that we can represent a set of rows purely as a set of their
|
|
# pivot indices.
|
|
pivot_row_map = {}
|
|
|
|
# Set of pivot indices for rows that are fully reduced to a single nonzero.
|
|
reduced_pivots = set()
|
|
|
|
# Set of pivot indices for rows not fully reduced
|
|
nonreduced_pivots = set()
|
|
|
|
# Map from column index to a set of pivot indices representing the rows
|
|
# that have a nonzero at that column.
|
|
nonzero_columns = defaultdict(set)
|
|
|
|
while Arows:
|
|
# Select pivot element and row
|
|
Ai = Arows.pop()
|
|
|
|
# Nonzero columns from fully reduced pivot rows can be removed
|
|
Ai = {j: Aij for j, Aij in Ai.items() if j not in reduced_pivots}
|
|
|
|
# Others require full row cancellation
|
|
for j in nonreduced_pivots & set(Ai):
|
|
Aj = pivot_row_map[j]
|
|
Aij = Ai[j]
|
|
Ainz = set(Ai)
|
|
Ajnz = set(Aj)
|
|
for k in Ajnz - Ainz:
|
|
Ai[k] = - Aij * Aj[k]
|
|
Ai.pop(j)
|
|
Ainz.remove(j)
|
|
for k in Ajnz & Ainz:
|
|
Aik = Ai[k] - Aij * Aj[k]
|
|
if Aik:
|
|
Ai[k] = Aik
|
|
else:
|
|
Ai.pop(k)
|
|
|
|
# We have now cancelled previously seen pivots from Ai.
|
|
# If it is zero then discard it.
|
|
if not Ai:
|
|
continue
|
|
|
|
# Choose a pivot from Ai:
|
|
j = min(Ai)
|
|
Aij = Ai[j]
|
|
pivot_row_map[j] = Ai
|
|
Ainz = set(Ai)
|
|
|
|
# Normalise the pivot row to make the pivot 1.
|
|
#
|
|
# This approach is slow for some domains. Cross cancellation might be
|
|
# better for e.g. QQ(x) with division delayed to the final steps.
|
|
Aijinv = Aij**-1
|
|
for l in Ai:
|
|
Ai[l] *= Aijinv
|
|
|
|
# Use Aij to cancel column j from all previously seen rows
|
|
for k in nonzero_columns.pop(j, ()):
|
|
Ak = pivot_row_map[k]
|
|
Akj = Ak[j]
|
|
Aknz = set(Ak)
|
|
for l in Ainz - Aknz:
|
|
Ak[l] = - Akj * Ai[l]
|
|
nonzero_columns[l].add(k)
|
|
Ak.pop(j)
|
|
Aknz.remove(j)
|
|
for l in Ainz & Aknz:
|
|
Akl = Ak[l] - Akj * Ai[l]
|
|
if Akl:
|
|
Ak[l] = Akl
|
|
else:
|
|
# Drop nonzero elements
|
|
Ak.pop(l)
|
|
if l != j:
|
|
nonzero_columns[l].remove(k)
|
|
if len(Ak) == 1:
|
|
reduced_pivots.add(k)
|
|
nonreduced_pivots.remove(k)
|
|
|
|
if len(Ai) == 1:
|
|
reduced_pivots.add(j)
|
|
else:
|
|
nonreduced_pivots.add(j)
|
|
for l in Ai:
|
|
if l != j:
|
|
nonzero_columns[l].add(j)
|
|
|
|
# All done!
|
|
pivots = sorted(reduced_pivots | nonreduced_pivots)
|
|
pivot2row = {p: n for n, p in enumerate(pivots)}
|
|
nonzero_columns = {c: {pivot2row[p] for p in s} for c, s in nonzero_columns.items()}
|
|
rows = [pivot_row_map[i] for i in pivots]
|
|
rref = dict(enumerate(rows))
|
|
return rref, pivots, nonzero_columns
|
|
|
|
|
|
def sdm_nullspace_from_rref(A, one, ncols, pivots, nonzero_cols):
|
|
"""Get nullspace from A which is in RREF"""
|
|
nonpivots = sorted(set(range(ncols)) - set(pivots))
|
|
|
|
K = []
|
|
for j in nonpivots:
|
|
Kj = {j:one}
|
|
for i in nonzero_cols.get(j, ()):
|
|
Kj[pivots[i]] = -A[i][j]
|
|
K.append(Kj)
|
|
|
|
return K, nonpivots
|
|
|
|
|
|
def sdm_particular_from_rref(A, ncols, pivots):
|
|
"""Get a particular solution from A which is in RREF"""
|
|
P = {}
|
|
for i, j in enumerate(pivots):
|
|
Ain = A[i].get(ncols-1, None)
|
|
if Ain is not None:
|
|
P[j] = Ain / A[i][j]
|
|
return P
|