I am done

This commit is contained in:
2024-10-30 22:14:35 +01:00
parent 720dc28c09
commit 40e2a747cf
36901 changed files with 5011519 additions and 0 deletions

View File

@ -0,0 +1,15 @@
"""
sympy.polys.matrices package.
The main export from this package is the DomainMatrix class which is a
lower-level implementation of matrices based on the polys Domains. This
implementation is typically a lot faster than SymPy's standard Matrix class
but is a work in progress and is still experimental.
"""
from .domainmatrix import DomainMatrix, DM
__all__ = [
'DomainMatrix', 'DM',
]

View File

@ -0,0 +1,898 @@
#
# sympy.polys.matrices.dfm
#
# This modules defines the DFM class which is a wrapper for dense flint
# matrices as found in python-flint.
#
# As of python-flint 0.4.1 matrices over the following domains can be supported
# by python-flint:
#
# ZZ: flint.fmpz_mat
# QQ: flint.fmpq_mat
# GF(p): flint.nmod_mat (p prime and p < ~2**62)
#
# The underlying flint library has many more domains, but these are not yet
# supported by python-flint.
#
# The DFM class is a wrapper for the flint matrices and provides a common
# interface for all supported domains that is interchangeable with the DDM
# and SDM classes so that DomainMatrix can be used with any as its internal
# matrix representation.
#
# TODO:
#
# Implement the following methods that are provided by python-flint:
#
# - hnf (Hermite normal form)
# - snf (Smith normal form)
# - minpoly
# - is_hnf
# - is_snf
# - rank
#
# The other types DDM and SDM do not have these methods and the algorithms
# for hnf, snf and rank are already implemented. Algorithms for minpoly,
# is_hnf and is_snf would need to be added.
#
# Add more methods to python-flint to expose more of Flint's functionality
# and also to make some of the above methods simpler or more efficient e.g.
# slicing, fancy indexing etc.
from sympy.external.gmpy import GROUND_TYPES
from sympy.external.importtools import import_module
from sympy.utilities.decorator import doctest_depends_on
from sympy.polys.domains import ZZ, QQ
from .exceptions import (
DMBadInputError,
DMDomainError,
DMNonSquareMatrixError,
DMNonInvertibleMatrixError,
DMRankError,
DMShapeError,
DMValueError,
)
if GROUND_TYPES != 'flint':
__doctest_skip__ = ['*']
flint = import_module('flint')
__all__ = ['DFM']
@doctest_depends_on(ground_types=['flint'])
class DFM:
"""
Dense FLINT matrix. This class is a wrapper for matrices from python-flint.
>>> from sympy.polys.domains import ZZ
>>> from sympy.polys.matrices.dfm import DFM
>>> dfm = DFM([[ZZ(1), ZZ(2)], [ZZ(3), ZZ(4)]], (2, 2), ZZ)
>>> dfm
[[1, 2], [3, 4]]
>>> dfm.rep
[1, 2]
[3, 4]
>>> type(dfm.rep) # doctest: +SKIP
<class 'flint._flint.fmpz_mat'>
Usually, the DFM class is not instantiated directly, but is created as the
internal representation of :class:`~.DomainMatrix`. When
`SYMPY_GROUND_TYPES` is set to `flint` and `python-flint` is installed, the
:class:`DFM` class is used automatically as the internal representation of
:class:`~.DomainMatrix` in dense format if the domain is supported by
python-flint.
>>> from sympy.polys.matrices.domainmatrix import DM
>>> dM = DM([[1, 2], [3, 4]], ZZ)
>>> dM.rep
[[1, 2], [3, 4]]
A :class:`~.DomainMatrix` can be converted to :class:`DFM` by calling the
:meth:`to_dfm` method:
>>> dM.to_dfm()
[[1, 2], [3, 4]]
"""
fmt = 'dense'
is_DFM = True
is_DDM = False
def __new__(cls, rowslist, shape, domain):
"""Construct from a nested list."""
flint_mat = cls._get_flint_func(domain)
if 0 not in shape:
try:
rep = flint_mat(rowslist)
except (ValueError, TypeError):
raise DMBadInputError(f"Input should be a list of list of {domain}")
else:
rep = flint_mat(*shape)
return cls._new(rep, shape, domain)
@classmethod
def _new(cls, rep, shape, domain):
"""Internal constructor from a flint matrix."""
cls._check(rep, shape, domain)
obj = object.__new__(cls)
obj.rep = rep
obj.shape = obj.rows, obj.cols = shape
obj.domain = domain
return obj
def _new_rep(self, rep):
"""Create a new DFM with the same shape and domain but a new rep."""
return self._new(rep, self.shape, self.domain)
@classmethod
def _check(cls, rep, shape, domain):
repshape = (rep.nrows(), rep.ncols())
if repshape != shape:
raise DMBadInputError("Shape of rep does not match shape of DFM")
if domain == ZZ and not isinstance(rep, flint.fmpz_mat):
raise RuntimeError("Rep is not a flint.fmpz_mat")
elif domain == QQ and not isinstance(rep, flint.fmpq_mat):
raise RuntimeError("Rep is not a flint.fmpq_mat")
elif domain not in (ZZ, QQ):
raise NotImplementedError("Only ZZ and QQ are supported by DFM")
@classmethod
def _supports_domain(cls, domain):
"""Return True if the given domain is supported by DFM."""
return domain in (ZZ, QQ)
@classmethod
def _get_flint_func(cls, domain):
"""Return the flint matrix class for the given domain."""
if domain == ZZ:
return flint.fmpz_mat
elif domain == QQ:
return flint.fmpq_mat
else:
raise NotImplementedError("Only ZZ and QQ are supported by DFM")
@property
def _func(self):
"""Callable to create a flint matrix of the same domain."""
return self._get_flint_func(self.domain)
def __str__(self):
"""Return ``str(self)``."""
return str(self.to_ddm())
def __repr__(self):
"""Return ``repr(self)``."""
return f'DFM{repr(self.to_ddm())[3:]}'
def __eq__(self, other):
"""Return ``self == other``."""
if not isinstance(other, DFM):
return NotImplemented
# Compare domains first because we do *not* want matrices with
# different domains to be equal but e.g. a flint fmpz_mat and fmpq_mat
# with the same entries will compare equal.
return self.domain == other.domain and self.rep == other.rep
@classmethod
def from_list(cls, rowslist, shape, domain):
"""Construct from a nested list."""
return cls(rowslist, shape, domain)
def to_list(self):
"""Convert to a nested list."""
return self.rep.tolist()
def copy(self):
"""Return a copy of self."""
return self._new_rep(self._func(self.rep))
def to_ddm(self):
"""Convert to a DDM."""
return DDM.from_list(self.to_list(), self.shape, self.domain)
def to_sdm(self):
"""Convert to a SDM."""
return SDM.from_list(self.to_list(), self.shape, self.domain)
def to_dfm(self):
"""Return self."""
return self
def to_dfm_or_ddm(self):
"""
Convert to a :class:`DFM`.
This :class:`DFM` method exists to parallel the :class:`~.DDM` and
:class:`~.SDM` methods. For :class:`DFM` it will always return self.
See Also
========
to_ddm
to_sdm
sympy.polys.matrices.domainmatrix.DomainMatrix.to_dfm_or_ddm
"""
return self
@classmethod
def from_ddm(cls, ddm):
"""Convert from a DDM."""
return cls.from_list(ddm.to_list(), ddm.shape, ddm.domain)
@classmethod
def from_list_flat(cls, elements, shape, domain):
"""Inverse of :meth:`to_list_flat`."""
func = cls._get_flint_func(domain)
try:
rep = func(*shape, elements)
except ValueError:
raise DMBadInputError(f"Incorrect number of elements for shape {shape}")
except TypeError:
raise DMBadInputError(f"Input should be a list of {domain}")
return cls(rep, shape, domain)
def to_list_flat(self):
"""Convert to a flat list."""
return self.rep.entries()
def to_flat_nz(self):
"""Convert to a flat list of non-zeros."""
return self.to_ddm().to_flat_nz()
@classmethod
def from_flat_nz(cls, elements, data, domain):
"""Inverse of :meth:`to_flat_nz`."""
return DDM.from_flat_nz(elements, data, domain).to_dfm()
def to_dod(self):
"""Convert to a DOD."""
return self.to_ddm().to_dod()
@classmethod
def from_dod(cls, dod, shape, domain):
"""Inverse of :meth:`to_dod`."""
return DDM.from_dod(dod, shape, domain).to_dfm()
def to_dok(self):
"""Convert to a DOK."""
return self.to_ddm().to_dok()
@classmethod
def from_dok(cls, dok, shape, domain):
"""Inverse of :math:`to_dod`."""
return DDM.from_dok(dok, shape, domain).to_dfm()
def iter_values(self):
"""Iterater over the non-zero values of the matrix."""
m, n = self.shape
rep = self.rep
for i in range(m):
for j in range(n):
repij = rep[i, j]
if repij:
yield rep[i, j]
def iter_items(self):
"""Iterate over indices and values of nonzero elements of the matrix."""
m, n = self.shape
rep = self.rep
for i in range(m):
for j in range(n):
repij = rep[i, j]
if repij:
yield ((i, j), repij)
def convert_to(self, domain):
"""Convert to a new domain."""
if domain == self.domain:
return self.copy()
elif domain == QQ and self.domain == ZZ:
return self._new(flint.fmpq_mat(self.rep), self.shape, domain)
elif domain == ZZ and self.domain == QQ:
# XXX: python-flint has no fmpz_mat.from_fmpq_mat
return self.to_ddm().convert_to(domain).to_dfm()
else:
# It is the callers responsibility to convert to DDM before calling
# this method if the domain is not supported by DFM.
raise NotImplementedError("Only ZZ and QQ are supported by DFM")
def getitem(self, i, j):
"""Get the ``(i, j)``-th entry."""
# XXX: flint matrices do not support negative indices
# XXX: They also raise ValueError instead of IndexError
m, n = self.shape
if i < 0:
i += m
if j < 0:
j += n
try:
return self.rep[i, j]
except ValueError:
raise IndexError(f"Invalid indices ({i}, {j}) for Matrix of shape {self.shape}")
def setitem(self, i, j, value):
"""Set the ``(i, j)``-th entry."""
# XXX: flint matrices do not support negative indices
# XXX: They also raise ValueError instead of IndexError
m, n = self.shape
if i < 0:
i += m
if j < 0:
j += n
try:
self.rep[i, j] = value
except ValueError:
raise IndexError(f"Invalid indices ({i}, {j}) for Matrix of shape {self.shape}")
def _extract(self, i_indices, j_indices):
"""Extract a submatrix with no checking."""
# Indices must be positive and in range.
M = self.rep
lol = [[M[i, j] for j in j_indices] for i in i_indices]
shape = (len(i_indices), len(j_indices))
return self.from_list(lol, shape, self.domain)
def extract(self, rowslist, colslist):
"""Extract a submatrix."""
# XXX: flint matrices do not support fancy indexing or negative indices
#
# Check and convert negative indices before calling _extract.
m, n = self.shape
new_rows = []
new_cols = []
for i in rowslist:
if i < 0:
i_pos = i + m
else:
i_pos = i
if not 0 <= i_pos < m:
raise IndexError(f"Invalid row index {i} for Matrix of shape {self.shape}")
new_rows.append(i_pos)
for j in colslist:
if j < 0:
j_pos = j + n
else:
j_pos = j
if not 0 <= j_pos < n:
raise IndexError(f"Invalid column index {j} for Matrix of shape {self.shape}")
new_cols.append(j_pos)
return self._extract(new_rows, new_cols)
def extract_slice(self, rowslice, colslice):
"""Slice a DFM."""
# XXX: flint matrices do not support slicing
m, n = self.shape
i_indices = range(m)[rowslice]
j_indices = range(n)[colslice]
return self._extract(i_indices, j_indices)
def neg(self):
"""Negate a DFM matrix."""
return self._new_rep(-self.rep)
def add(self, other):
"""Add two DFM matrices."""
return self._new_rep(self.rep + other.rep)
def sub(self, other):
"""Subtract two DFM matrices."""
return self._new_rep(self.rep - other.rep)
def mul(self, other):
"""Multiply a DFM matrix from the right by a scalar."""
return self._new_rep(self.rep * other)
def rmul(self, other):
"""Multiply a DFM matrix from the left by a scalar."""
return self._new_rep(other * self.rep)
def mul_elementwise(self, other):
"""Elementwise multiplication of two DFM matrices."""
# XXX: flint matrices do not support elementwise multiplication
return self.to_ddm().mul_elementwise(other.to_ddm()).to_dfm()
def matmul(self, other):
"""Multiply two DFM matrices."""
shape = (self.rows, other.cols)
return self._new(self.rep * other.rep, shape, self.domain)
# XXX: For the most part DomainMatrix does not expect DDM, SDM, or DFM to
# have arithmetic operators defined. The only exception is negation.
# Perhaps that should be removed.
def __neg__(self):
"""Negate a DFM matrix."""
return self.neg()
@classmethod
def zeros(cls, shape, domain):
"""Return a zero DFM matrix."""
func = cls._get_flint_func(domain)
return cls._new(func(*shape), shape, domain)
# XXX: flint matrices do not have anything like ones or eye
# In the methods below we convert to DDM and then back to DFM which is
# probably about as efficient as implementing these methods directly.
@classmethod
def ones(cls, shape, domain):
"""Return a one DFM matrix."""
# XXX: flint matrices do not have anything like ones
return DDM.ones(shape, domain).to_dfm()
@classmethod
def eye(cls, n, domain):
"""Return the identity matrix of size n."""
# XXX: flint matrices do not have anything like eye
return DDM.eye(n, domain).to_dfm()
@classmethod
def diag(cls, elements, domain):
"""Return a diagonal matrix."""
return DDM.diag(elements, domain).to_dfm()
def applyfunc(self, func, domain):
"""Apply a function to each entry of a DFM matrix."""
return self.to_ddm().applyfunc(func, domain).to_dfm()
def transpose(self):
"""Transpose a DFM matrix."""
return self._new(self.rep.transpose(), (self.cols, self.rows), self.domain)
def hstack(self, *others):
"""Horizontally stack matrices."""
return self.to_ddm().hstack(*[o.to_ddm() for o in others]).to_dfm()
def vstack(self, *others):
"""Vertically stack matrices."""
return self.to_ddm().vstack(*[o.to_ddm() for o in others]).to_dfm()
def diagonal(self):
"""Return the diagonal of a DFM matrix."""
M = self.rep
m, n = self.shape
return [M[i, i] for i in range(min(m, n))]
def is_upper(self):
"""Return ``True`` if the matrix is upper triangular."""
M = self.rep
for i in range(self.rows):
for j in range(i):
if M[i, j]:
return False
return True
def is_lower(self):
"""Return ``True`` if the matrix is lower triangular."""
M = self.rep
for i in range(self.rows):
for j in range(i + 1, self.cols):
if M[i, j]:
return False
return True
def is_diagonal(self):
"""Return ``True`` if the matrix is diagonal."""
return self.is_upper() and self.is_lower()
def is_zero_matrix(self):
"""Return ``True`` if the matrix is the zero matrix."""
M = self.rep
for i in range(self.rows):
for j in range(self.cols):
if M[i, j]:
return False
return True
def nnz(self):
"""Return the number of non-zero elements in the matrix."""
return self.to_ddm().nnz()
def scc(self):
"""Return the strongly connected components of the matrix."""
return self.to_ddm().scc()
@doctest_depends_on(ground_types='flint')
def det(self):
"""
Compute the determinant of the matrix using FLINT.
Examples
========
>>> from sympy import Matrix
>>> M = Matrix([[1, 2], [3, 4]])
>>> dfm = M.to_DM().to_dfm()
>>> dfm
[[1, 2], [3, 4]]
>>> dfm.det()
-2
Notes
=====
Calls the ``.det()`` method of the underlying FLINT matrix.
For :ref:`ZZ` or :ref:`QQ` this calls ``fmpz_mat_det`` or
``fmpq_mat_det`` respectively.
At the time of writing the implementation of ``fmpz_mat_det`` uses one
of several algorithms depending on the size of the matrix and bit size
of the entries. The algorithms used are:
- Cofactor for very small (up to 4x4) matrices.
- Bareiss for small (up to 25x25) matrices.
- Modular algorithms for larger matrices (up to 60x60) or for larger
matrices with large bit sizes.
- Modular "accelerated" for larger matrices (60x60 upwards) if the bit
size is smaller than the dimensions of the matrix.
The implementation of ``fmpq_mat_det`` clears denominators from each
row (not the whole matrix) and then calls ``fmpz_mat_det`` and divides
by the product of the denominators.
See Also
========
sympy.polys.matrices.domainmatrix.DomainMatrix.det
Higher level interface to compute the determinant of a matrix.
"""
# XXX: At least the first three algorithms described above should also
# be implemented in the pure Python DDM and SDM classes which at the
# time of writng just use Bareiss for all matrices and domains.
# Probably in Python the thresholds would be different though.
return self.rep.det()
@doctest_depends_on(ground_types='flint')
def charpoly(self):
"""
Compute the characteristic polynomial of the matrix using FLINT.
Examples
========
>>> from sympy import Matrix
>>> M = Matrix([[1, 2], [3, 4]])
>>> dfm = M.to_DM().to_dfm() # need ground types = 'flint'
>>> dfm
[[1, 2], [3, 4]]
>>> dfm.charpoly()
[1, -5, -2]
Notes
=====
Calls the ``.charpoly()`` method of the underlying FLINT matrix.
For :ref:`ZZ` or :ref:`QQ` this calls ``fmpz_mat_charpoly`` or
``fmpq_mat_charpoly`` respectively.
At the time of writing the implementation of ``fmpq_mat_charpoly``
clears a denominator from the whole matrix and then calls
``fmpz_mat_charpoly``. The coefficients of the characteristic
polynomial are then multiplied by powers of the denominator.
The ``fmpz_mat_charpoly`` method uses a modular algorithm with CRT
reconstruction. The modular algorithm uses ``nmod_mat_charpoly`` which
uses Berkowitz for small matrices and non-prime moduli or otherwise
the Danilevsky method.
See Also
========
sympy.polys.matrices.domainmatrix.DomainMatrix.charpoly
Higher level interface to compute the characteristic polynomial of
a matrix.
"""
# FLINT polynomial coefficients are in reverse order compared to SymPy.
return self.rep.charpoly().coeffs()[::-1]
@doctest_depends_on(ground_types='flint')
def inv(self):
"""
Compute the inverse of a matrix using FLINT.
Examples
========
>>> from sympy import Matrix, QQ
>>> M = Matrix([[1, 2], [3, 4]])
>>> dfm = M.to_DM().to_dfm().convert_to(QQ)
>>> dfm
[[1, 2], [3, 4]]
>>> dfm.inv()
[[-2, 1], [3/2, -1/2]]
>>> dfm.matmul(dfm.inv())
[[1, 0], [0, 1]]
Notes
=====
Calls the ``.inv()`` method of the underlying FLINT matrix.
For now this will raise an error if the domain is :ref:`ZZ` but will
use the FLINT method for :ref:`QQ`.
The FLINT methods for :ref:`ZZ` and :ref:`QQ` are ``fmpz_mat_inv`` and
``fmpq_mat_inv`` respectively. The ``fmpz_mat_inv`` method computes an
inverse with denominator. This is implemented by calling
``fmpz_mat_solve`` (see notes in :meth:`lu_solve` about the algorithm).
The ``fmpq_mat_inv`` method clears denominators from each row and then
multiplies those into the rhs identity matrix before calling
``fmpz_mat_solve``.
See Also
========
sympy.polys.matrices.domainmatrix.DomainMatrix.inv
Higher level method for computing the inverse of a matrix.
"""
# TODO: Implement similar algorithms for DDM and SDM.
#
# XXX: The flint fmpz_mat and fmpq_mat inv methods both return fmpq_mat
# by default. The fmpz_mat method has an optional argument to return
# fmpz_mat instead for unimodular matrices.
#
# The convention in DomainMatrix is to raise an error if the matrix is
# not over a field regardless of whether the matrix is invertible over
# its domain or over any associated field. Maybe DomainMatrix.inv
# should be changed to always return a matrix over an associated field
# except with a unimodular argument for returning an inverse over a
# ring if possible.
#
# For now we follow the existing DomainMatrix convention...
K = self.domain
m, n = self.shape
if m != n:
raise DMNonSquareMatrixError("cannot invert a non-square matrix")
if K == ZZ:
raise DMDomainError("field expected, got %s" % K)
elif K == QQ:
try:
return self._new_rep(self.rep.inv())
except ZeroDivisionError:
raise DMNonInvertibleMatrixError("matrix is not invertible")
else:
# If more domains are added for DFM then we will need to consider
# what happens here.
raise NotImplementedError("DFM.inv() is not implemented for %s" % K)
def lu(self):
"""Return the LU decomposition of the matrix."""
L, U, swaps = self.to_ddm().lu()
return L.to_dfm(), U.to_dfm(), swaps
# XXX: The lu_solve function should be renamed to solve. Whether or not it
# uses an LU decomposition is an implementation detail. A method called
# lu_solve would make sense for a situation in which an LU decomposition is
# reused several times to solve iwth different rhs but that would imply a
# different call signature.
#
# The underlying python-flint method has an algorithm= argument so we could
# use that and have e.g. solve_lu and solve_modular or perhaps also a
# method= argument to choose between the two. Flint itself has more
# possible algorithms to choose from than are exposed by python-flint.
@doctest_depends_on(ground_types='flint')
def lu_solve(self, rhs):
"""
Solve a matrix equation using FLINT.
Examples
========
>>> from sympy import Matrix, QQ
>>> M = Matrix([[1, 2], [3, 4]])
>>> dfm = M.to_DM().to_dfm().convert_to(QQ)
>>> dfm
[[1, 2], [3, 4]]
>>> rhs = Matrix([1, 2]).to_DM().to_dfm().convert_to(QQ)
>>> dfm.lu_solve(rhs)
[[0], [1/2]]
Notes
=====
Calls the ``.solve()`` method of the underlying FLINT matrix.
For now this will raise an error if the domain is :ref:`ZZ` but will
use the FLINT method for :ref:`QQ`.
The FLINT methods for :ref:`ZZ` and :ref:`QQ` are ``fmpz_mat_solve``
and ``fmpq_mat_solve`` respectively. The ``fmpq_mat_solve`` method
uses one of two algorithms:
- For small matrices (<25 rows) it clears denominators between the
matrix and rhs and uses ``fmpz_mat_solve``.
- For larger matrices it uses ``fmpq_mat_solve_dixon`` which is a
modular approach with CRT reconstruction over :ref:`QQ`.
The ``fmpz_mat_solve`` method uses one of four algorithms:
- For very small (<= 3x3) matrices it uses a Cramer's rule.
- For small (<= 15x15) matrices it uses a fraction-free LU solve.
- Otherwise it uses either Dixon or another multimodular approach.
See Also
========
sympy.polys.matrices.domainmatrix.DomainMatrix.lu_solve
Higher level interface to solve a matrix equation.
"""
if not self.domain == rhs.domain:
raise DMDomainError("Domains must match: %s != %s" % (self.domain, rhs.domain))
# XXX: As for inv we should consider whether to return a matrix over
# over an associated field or attempt to find a solution in the ring.
# For now we follow the existing DomainMatrix convention...
if not self.domain.is_Field:
raise DMDomainError("Field expected, got %s" % self.domain)
m, n = self.shape
j, k = rhs.shape
if m != j:
raise DMShapeError("Matrix size mismatch: %s * %s vs %s * %s" % (m, n, j, k))
sol_shape = (n, k)
# XXX: The Flint solve method only handles square matrices. Probably
# Flint has functions that could be used to solve non-square systems
# but they are not exposed in python-flint yet. Alternatively we could
# put something here using the features that are available like rref.
if m != n:
return self.to_ddm().lu_solve(rhs.to_ddm()).to_dfm()
try:
sol = self.rep.solve(rhs.rep)
except ZeroDivisionError:
raise DMNonInvertibleMatrixError("Matrix det == 0; not invertible.")
return self._new(sol, sol_shape, self.domain)
def nullspace(self):
"""Return a basis for the nullspace of the matrix."""
# Code to compute nullspace using flint:
#
# V, nullity = self.rep.nullspace()
# V_dfm = self._new_rep(V)._extract(range(self.rows), range(nullity))
#
# XXX: That gives the nullspace but does not give us nonpivots. So we
# use the slower DDM method anyway. It would be better to change the
# signature of the nullspace method to not return nonpivots.
#
# XXX: Also python-flint exposes a nullspace method for fmpz_mat but
# not for fmpq_mat. This is the reverse of the situation for DDM etc
# which only allow nullspace over a field. The nullspace method for
# DDM, SDM etc should be changed to allow nullspace over ZZ as well.
# The DomainMatrix nullspace method does allow the domain to be a ring
# but does not directly call the lower-level nullspace methods and uses
# rref_den instead. Nullspace methods should also be added to all
# matrix types in python-flint.
ddm, nonpivots = self.to_ddm().nullspace()
return ddm.to_dfm(), nonpivots
def nullspace_from_rref(self, pivots=None):
"""Return a basis for the nullspace of the matrix."""
# XXX: Use the flint nullspace method!!!
sdm, nonpivots = self.to_sdm().nullspace_from_rref(pivots=pivots)
return sdm.to_dfm(), nonpivots
def particular(self):
"""Return a particular solution to the system."""
return self.to_ddm().particular().to_dfm()
def _lll(self, transform=False, delta=0.99, eta=0.51, rep='zbasis', gram='approx'):
"""Call the fmpz_mat.lll() method but check rank to avoid segfaults."""
# XXX: There are tests that pass e.g. QQ(5,6) for delta. That fails
# with a TypeError in flint because if QQ is fmpq then conversion with
# float fails. We handle that here but there are two better fixes:
#
# - Make python-flint's fmpq convert with float(x)
# - Change the tests because delta should just be a float.
def to_float(x):
if QQ.of_type(x):
return float(x.numerator) / float(x.denominator)
else:
return float(x)
delta = to_float(delta)
eta = to_float(eta)
if not 0.25 < delta < 1:
raise DMValueError("delta must be between 0.25 and 1")
# XXX: The flint lll method segfaults if the matrix is not full rank.
m, n = self.shape
if self.rep.rank() != m:
raise DMRankError("Matrix must have full row rank for Flint LLL.")
# Actually call the flint method.
return self.rep.lll(transform=transform, delta=delta, eta=eta, rep=rep, gram=gram)
@doctest_depends_on(ground_types='flint')
def lll(self, delta=0.75):
"""Compute LLL-reduced basis using FLINT.
See :meth:`lll_transform` for more information.
Examples
========
>>> from sympy import Matrix
>>> M = Matrix([[1, 2, 3], [4, 5, 6]])
>>> M.to_DM().to_dfm().lll()
[[2, 1, 0], [-1, 1, 3]]
See Also
========
sympy.polys.matrices.domainmatrix.DomainMatrix.lll
Higher level interface to compute LLL-reduced basis.
lll_transform
Compute LLL-reduced basis and transform matrix.
"""
if self.domain != ZZ:
raise DMDomainError("ZZ expected, got %s" % self.domain)
elif self.rows > self.cols:
raise DMShapeError("Matrix must not have more rows than columns.")
rep = self._lll(delta=delta)
return self._new_rep(rep)
@doctest_depends_on(ground_types='flint')
def lll_transform(self, delta=0.75):
"""Compute LLL-reduced basis and transform using FLINT.
Examples
========
>>> from sympy import Matrix
>>> M = Matrix([[1, 2, 3], [4, 5, 6]]).to_DM().to_dfm()
>>> M_lll, T = M.lll_transform()
>>> M_lll
[[2, 1, 0], [-1, 1, 3]]
>>> T
[[-2, 1], [3, -1]]
>>> T.matmul(M) == M_lll
True
See Also
========
sympy.polys.matrices.domainmatrix.DomainMatrix.lll
Higher level interface to compute LLL-reduced basis.
lll
Compute LLL-reduced basis without transform matrix.
"""
if self.domain != ZZ:
raise DMDomainError("ZZ expected, got %s" % self.domain)
elif self.rows > self.cols:
raise DMShapeError("Matrix must not have more rows than columns.")
rep, T = self._lll(transform=True, delta=delta)
basis = self._new_rep(rep)
T_dfm = self._new(T, (self.rows, self.rows), self.domain)
return basis, T_dfm
# Avoid circular imports
from sympy.polys.matrices.ddm import DDM
from sympy.polys.matrices.ddm import SDM

View File

@ -0,0 +1,16 @@
from typing import TypeVar, Protocol
T = TypeVar('T')
class RingElement(Protocol):
"""A ring element.
Must support ``+``, ``-``, ``*``, ``**`` and ``-``.
"""
def __add__(self: T, other: T, /) -> T: ...
def __sub__(self: T, other: T, /) -> T: ...
def __mul__(self: T, other: T, /) -> T: ...
def __pow__(self: T, other: int, /) -> T: ...
def __neg__(self: T, /) -> T: ...

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,824 @@
"""
Module for the ddm_* routines for operating on a matrix in list of lists
matrix representation.
These routines are used internally by the DDM class which also provides a
friendlier interface for them. The idea here is to implement core matrix
routines in a way that can be applied to any simple list representation
without the need to use any particular matrix class. For example we can
compute the RREF of a matrix like:
>>> from sympy.polys.matrices.dense import ddm_irref
>>> M = [[1, 2, 3], [4, 5, 6]]
>>> pivots = ddm_irref(M)
>>> M
[[1.0, 0.0, -1.0], [0, 1.0, 2.0]]
These are lower-level routines that work mostly in place.The routines at this
level should not need to know what the domain of the elements is but should
ideally document what operations they will use and what functions they need to
be provided with.
The next-level up is the DDM class which uses these routines but wraps them up
with an interface that handles copying etc and keeps track of the Domain of
the elements of the matrix:
>>> from sympy.polys.domains import QQ
>>> from sympy.polys.matrices.ddm import DDM
>>> M = DDM([[QQ(1), QQ(2), QQ(3)], [QQ(4), QQ(5), QQ(6)]], (2, 3), QQ)
>>> M
[[1, 2, 3], [4, 5, 6]]
>>> Mrref, pivots = M.rref()
>>> Mrref
[[1, 0, -1], [0, 1, 2]]
"""
from __future__ import annotations
from operator import mul
from .exceptions import (
DMShapeError,
DMDomainError,
DMNonInvertibleMatrixError,
DMNonSquareMatrixError,
)
from typing import Sequence, TypeVar
from sympy.polys.matrices._typing import RingElement
#: Type variable for the elements of the matrix
T = TypeVar('T')
#: Type variable for the elements of the matrix that are in a ring
R = TypeVar('R', bound=RingElement)
def ddm_transpose(matrix: Sequence[Sequence[T]]) -> list[list[T]]:
"""matrix transpose"""
return list(map(list, zip(*matrix)))
def ddm_iadd(a: list[list[R]], b: Sequence[Sequence[R]]) -> None:
"""a += b"""
for ai, bi in zip(a, b):
for j, bij in enumerate(bi):
ai[j] += bij
def ddm_isub(a: list[list[R]], b: Sequence[Sequence[R]]) -> None:
"""a -= b"""
for ai, bi in zip(a, b):
for j, bij in enumerate(bi):
ai[j] -= bij
def ddm_ineg(a: list[list[R]]) -> None:
"""a <-- -a"""
for ai in a:
for j, aij in enumerate(ai):
ai[j] = -aij
def ddm_imul(a: list[list[R]], b: R) -> None:
"""a <-- a*b"""
for ai in a:
for j, aij in enumerate(ai):
ai[j] = aij * b
def ddm_irmul(a: list[list[R]], b: R) -> None:
"""a <-- b*a"""
for ai in a:
for j, aij in enumerate(ai):
ai[j] = b * aij
def ddm_imatmul(
a: list[list[R]], b: Sequence[Sequence[R]], c: Sequence[Sequence[R]]
) -> None:
"""a += b @ c"""
cT = list(zip(*c))
for bi, ai in zip(b, a):
for j, cTj in enumerate(cT):
ai[j] = sum(map(mul, bi, cTj), ai[j])
def ddm_irref(a, _partial_pivot=False):
"""In-place reduced row echelon form of a matrix.
Compute the reduced row echelon form of $a$. Modifies $a$ in place and
returns a list of the pivot columns.
Uses naive Gauss-Jordan elimination in the ground domain which must be a
field.
This routine is only really suitable for use with simple field domains like
:ref:`GF(p)`, :ref:`QQ` and :ref:`QQ(a)` although even for :ref:`QQ` with
larger matrices it is possibly more efficient to use fraction free
approaches.
This method is not suitable for use with rational function fields
(:ref:`K(x)`) because the elements will blowup leading to costly gcd
operations. In this case clearing denominators and using fraction free
approaches is likely to be more efficient.
For inexact numeric domains like :ref:`RR` and :ref:`CC` pass
``_partial_pivot=True`` to use partial pivoting to control rounding errors.
Examples
========
>>> from sympy.polys.matrices.dense import ddm_irref
>>> from sympy import QQ
>>> M = [[QQ(1), QQ(2), QQ(3)], [QQ(4), QQ(5), QQ(6)]]
>>> pivots = ddm_irref(M)
>>> M
[[1, 0, -1], [0, 1, 2]]
>>> pivots
[0, 1]
See Also
========
sympy.polys.matrices.domainmatrix.DomainMatrix.rref
Higher level interface to this routine.
ddm_irref_den
The fraction free version of this routine.
sdm_irref
A sparse version of this routine.
References
==========
.. [1] https://en.wikipedia.org/wiki/Row_echelon_form#Reduced_row_echelon_form
"""
# We compute aij**-1 below and then use multiplication instead of division
# in the innermost loop. The domain here is a field so either operation is
# defined. There are significant performance differences for some domains
# though. In the case of e.g. QQ or QQ(x) inversion is free but
# multiplication and division have the same cost so it makes no difference.
# In cases like GF(p), QQ<sqrt(2)>, RR or CC though multiplication is
# faster than division so reusing a precomputed inverse for many
# multiplications can be a lot faster. The biggest win is QQ<a> when
# deg(minpoly(a)) is large.
#
# With domains like QQ(x) this can perform badly for other reasons.
# Typically the initial matrix has simple denominators and the
# fraction-free approach with exquo (ddm_irref_den) will preserve that
# property throughout. The method here causes denominator blowup leading to
# expensive gcd reductions in the intermediate expressions. With many
# generators like QQ(x,y,z,...) this is extremely bad.
#
# TODO: Use a nontrivial pivoting strategy to control intermediate
# expression growth. Rearranging rows and/or columns could defer the most
# complicated elements until the end. If the first pivot is a
# complicated/large element then the first round of reduction will
# immediately introduce expression blowup across the whole matrix.
# a is (m x n)
m = len(a)
if not m:
return []
n = len(a[0])
i = 0
pivots = []
for j in range(n):
# Proper pivoting should be used for all domains for performance
# reasons but it is only strictly needed for RR and CC (and possibly
# other domains like RR(x)). This path is used by DDM.rref() if the
# domain is RR or CC. It uses partial (row) pivoting based on the
# absolute value of the pivot candidates.
if _partial_pivot:
ip = max(range(i, m), key=lambda ip: abs(a[ip][j]))
a[i], a[ip] = a[ip], a[i]
# pivot
aij = a[i][j]
# zero-pivot
if not aij:
for ip in range(i+1, m):
aij = a[ip][j]
# row-swap
if aij:
a[i], a[ip] = a[ip], a[i]
break
else:
# next column
continue
# normalise row
ai = a[i]
aijinv = aij**-1
for l in range(j, n):
ai[l] *= aijinv # ai[j] = one
# eliminate above and below to the right
for k, ak in enumerate(a):
if k == i or not ak[j]:
continue
akj = ak[j]
ak[j] -= akj # ak[j] = zero
for l in range(j+1, n):
ak[l] -= akj * ai[l]
# next row
pivots.append(j)
i += 1
# no more rows?
if i >= m:
break
return pivots
def ddm_irref_den(a, K):
"""a <-- rref(a); return (den, pivots)
Compute the fraction-free reduced row echelon form (RREF) of $a$. Modifies
$a$ in place and returns a tuple containing the denominator of the RREF and
a list of the pivot columns.
Explanation
===========
The algorithm used is the fraction-free version of Gauss-Jordan elimination
described as FFGJ in [1]_. Here it is modified to handle zero or missing
pivots and to avoid redundant arithmetic.
The domain $K$ must support exact division (``K.exquo``) but does not need
to be a field. This method is suitable for most exact rings and fields like
:ref:`ZZ`, :ref:`QQ` and :ref:`QQ(a)`. In the case of :ref:`QQ` or
:ref:`K(x)` it might be more efficient to clear denominators and use
:ref:`ZZ` or :ref:`K[x]` instead.
For inexact domains like :ref:`RR` and :ref:`CC` use ``ddm_irref`` instead.
Examples
========
>>> from sympy.polys.matrices.dense import ddm_irref_den
>>> from sympy import ZZ, Matrix
>>> M = [[ZZ(1), ZZ(2), ZZ(3)], [ZZ(4), ZZ(5), ZZ(6)]]
>>> den, pivots = ddm_irref_den(M, ZZ)
>>> M
[[-3, 0, 3], [0, -3, -6]]
>>> den
-3
>>> pivots
[0, 1]
>>> Matrix(M).rref()[0]
Matrix([
[1, 0, -1],
[0, 1, 2]])
See Also
========
ddm_irref
A version of this routine that uses field division.
sdm_irref
A sparse version of :func:`ddm_irref`.
sdm_rref_den
A sparse version of :func:`ddm_irref_den`.
sympy.polys.matrices.domainmatrix.DomainMatrix.rref_den
Higher level interface.
References
==========
.. [1] Fraction-free algorithms for linear and polynomial equations.
George C. Nakos , Peter R. Turner , Robert M. Williams.
https://dl.acm.org/doi/10.1145/271130.271133
"""
#
# A simpler presentation of this algorithm is given in [1]:
#
# Given an n x n matrix A and n x 1 matrix b:
#
# for i in range(n):
# if i != 0:
# d = a[i-1][i-1]
# for j in range(n):
# if j == i:
# continue
# b[j] = a[i][i]*b[j] - a[j][i]*b[i]
# for k in range(n):
# a[j][k] = a[i][i]*a[j][k] - a[j][i]*a[i][k]
# if i != 0:
# a[j][k] /= d
#
# Our version here is a bit more complicated because:
#
# 1. We use row-swaps to avoid zero pivots.
# 2. We allow for some columns to be missing pivots.
# 3. We avoid a lot of redundant arithmetic.
#
# TODO: Use a non-trivial pivoting strategy. Even just row swapping makes a
# big difference to performance if e.g. the upper-left entry of the matrix
# is a huge polynomial.
# a is (m x n)
m = len(a)
if not m:
return K.one, []
n = len(a[0])
d = None
pivots = []
no_pivots = []
# i, j will be the row and column indices of the current pivot
i = 0
for j in range(n):
# next pivot?
aij = a[i][j]
# swap rows if zero
if not aij:
for ip in range(i+1, m):
aij = a[ip][j]
# row-swap
if aij:
a[i], a[ip] = a[ip], a[i]
break
else:
# go to next column
no_pivots.append(j)
continue
# Now aij is the pivot and i,j are the row and column. We need to clear
# the column above and below but we also need to keep track of the
# denominator of the RREF which means also multiplying everything above
# and to the left by the current pivot aij and dividing by d (which we
# multiplied everything by in the previous iteration so this is an
# exact division).
#
# First handle the upper left corner which is usually already diagonal
# with all diagonal entries equal to the current denominator but there
# can be other non-zero entries in any column that has no pivot.
# Update previous pivots in the matrix
if pivots:
pivot_val = aij * a[0][pivots[0]]
# Divide out the common factor
if d is not None:
pivot_val = K.exquo(pivot_val, d)
# Could defer this until the end but it is pretty cheap and
# helps when debugging.
for ip, jp in enumerate(pivots):
a[ip][jp] = pivot_val
# Update columns without pivots
for jnp in no_pivots:
for ip in range(i):
aijp = a[ip][jnp]
if aijp:
aijp *= aij
if d is not None:
aijp = K.exquo(aijp, d)
a[ip][jnp] = aijp
# Eliminate above, below and to the right as in ordinary division free
# Gauss-Jordan elmination except also dividing out d from every entry.
for jp, aj in enumerate(a):
# Skip the current row
if jp == i:
continue
# Eliminate to the right in all rows
for kp in range(j+1, n):
ajk = aij * aj[kp] - aj[j] * a[i][kp]
if d is not None:
ajk = K.exquo(ajk, d)
aj[kp] = ajk
# Set to zero above and below the pivot
aj[j] = K.zero
# next row
pivots.append(j)
i += 1
# no more rows left?
if i >= m:
break
if not K.is_one(aij):
d = aij
else:
d = None
if not pivots:
denom = K.one
else:
denom = a[0][pivots[0]]
return denom, pivots
def ddm_idet(a, K):
"""a <-- echelon(a); return det
Explanation
===========
Compute the determinant of $a$ using the Bareiss fraction-free algorithm.
The matrix $a$ is modified in place. Its diagonal elements are the
determinants of the leading principal minors. The determinant of $a$ is
returned.
The domain $K$ must support exact division (``K.exquo``). This method is
suitable for most exact rings and fields like :ref:`ZZ`, :ref:`QQ` and
:ref:`QQ(a)` but not for inexact domains like :ref:`RR` and :ref:`CC`.
Examples
========
>>> from sympy import ZZ
>>> from sympy.polys.matrices.ddm import ddm_idet
>>> a = [[ZZ(1), ZZ(2), ZZ(3)], [ZZ(4), ZZ(5), ZZ(6)], [ZZ(7), ZZ(8), ZZ(9)]]
>>> a
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> ddm_idet(a, ZZ)
0
>>> a
[[1, 2, 3], [4, -3, -6], [7, -6, 0]]
>>> [a[i][i] for i in range(len(a))]
[1, -3, 0]
See Also
========
sympy.polys.matrices.domainmatrix.DomainMatrix.det
References
==========
.. [1] https://en.wikipedia.org/wiki/Bareiss_algorithm
.. [2] https://www.math.usm.edu/perry/Research/Thesis_DRL.pdf
"""
# Bareiss algorithm
# https://www.math.usm.edu/perry/Research/Thesis_DRL.pdf
# a is (m x n)
m = len(a)
if not m:
return K.one
n = len(a[0])
exquo = K.exquo
# uf keeps track of the sign change from row swaps
uf = K.one
for k in range(n-1):
if not a[k][k]:
for i in range(k+1, n):
if a[i][k]:
a[k], a[i] = a[i], a[k]
uf = -uf
break
else:
return K.zero
akkm1 = a[k-1][k-1] if k else K.one
for i in range(k+1, n):
for j in range(k+1, n):
a[i][j] = exquo(a[i][j]*a[k][k] - a[i][k]*a[k][j], akkm1)
return uf * a[-1][-1]
def ddm_iinv(ainv, a, K):
"""ainv <-- inv(a)
Compute the inverse of a matrix $a$ over a field $K$ using Gauss-Jordan
elimination. The result is stored in $ainv$.
Uses division in the ground domain which should be an exact field.
Examples
========
>>> from sympy.polys.matrices.ddm import ddm_iinv, ddm_imatmul
>>> from sympy import QQ
>>> a = [[QQ(1), QQ(2)], [QQ(3), QQ(4)]]
>>> ainv = [[None, None], [None, None]]
>>> ddm_iinv(ainv, a, QQ)
>>> ainv
[[-2, 1], [3/2, -1/2]]
>>> result = [[QQ(0), QQ(0)], [QQ(0), QQ(0)]]
>>> ddm_imatmul(result, a, ainv)
>>> result
[[1, 0], [0, 1]]
See Also
========
ddm_irref: the underlying routine.
"""
if not K.is_Field:
raise DMDomainError('Not a field')
# a is (m x n)
m = len(a)
if not m:
return
n = len(a[0])
if m != n:
raise DMNonSquareMatrixError
eye = [[K.one if i==j else K.zero for j in range(n)] for i in range(n)]
Aaug = [row + eyerow for row, eyerow in zip(a, eye)]
pivots = ddm_irref(Aaug)
if pivots != list(range(n)):
raise DMNonInvertibleMatrixError('Matrix det == 0; not invertible.')
ainv[:] = [row[n:] for row in Aaug]
def ddm_ilu_split(L, U, K):
"""L, U <-- LU(U)
Compute the LU decomposition of a matrix $L$ in place and store the lower
and upper triangular matrices in $L$ and $U$, respectively. Returns a list
of row swaps that were performed.
Uses division in the ground domain which should be an exact field.
Examples
========
>>> from sympy.polys.matrices.ddm import ddm_ilu_split
>>> from sympy import QQ
>>> L = [[QQ(0), QQ(0)], [QQ(0), QQ(0)]]
>>> U = [[QQ(1), QQ(2)], [QQ(3), QQ(4)]]
>>> swaps = ddm_ilu_split(L, U, QQ)
>>> swaps
[]
>>> L
[[0, 0], [3, 0]]
>>> U
[[1, 2], [0, -2]]
See Also
========
ddm_ilu
ddm_ilu_solve
"""
m = len(U)
if not m:
return []
n = len(U[0])
swaps = ddm_ilu(U)
zeros = [K.zero] * min(m, n)
for i in range(1, m):
j = min(i, n)
L[i][:j] = U[i][:j]
U[i][:j] = zeros[:j]
return swaps
def ddm_ilu(a):
"""a <-- LU(a)
Computes the LU decomposition of a matrix in place. Returns a list of
row swaps that were performed.
Uses division in the ground domain which should be an exact field.
This is only suitable for domains like :ref:`GF(p)`, :ref:`QQ`, :ref:`QQ_I`
and :ref:`QQ(a)`. With a rational function field like :ref:`K(x)` it is
better to clear denominators and use division-free algorithms. Pivoting is
used to avoid exact zeros but not for floating point accuracy so :ref:`RR`
and :ref:`CC` are not suitable (use :func:`ddm_irref` instead).
Examples
========
>>> from sympy.polys.matrices.dense import ddm_ilu
>>> from sympy import QQ
>>> a = [[QQ(1, 2), QQ(1, 3)], [QQ(1, 4), QQ(1, 5)]]
>>> swaps = ddm_ilu(a)
>>> swaps
[]
>>> a
[[1/2, 1/3], [1/2, 1/30]]
The same example using ``Matrix``:
>>> from sympy import Matrix, S
>>> M = Matrix([[S(1)/2, S(1)/3], [S(1)/4, S(1)/5]])
>>> L, U, swaps = M.LUdecomposition()
>>> L
Matrix([
[ 1, 0],
[1/2, 1]])
>>> U
Matrix([
[1/2, 1/3],
[ 0, 1/30]])
>>> swaps
[]
See Also
========
ddm_irref
ddm_ilu_solve
sympy.matrices.matrixbase.MatrixBase.LUdecomposition
"""
m = len(a)
if not m:
return []
n = len(a[0])
swaps = []
for i in range(min(m, n)):
if not a[i][i]:
for ip in range(i+1, m):
if a[ip][i]:
swaps.append((i, ip))
a[i], a[ip] = a[ip], a[i]
break
else:
# M = Matrix([[1, 0, 0, 0], [0, 0, 0, 0], [0, 0, 1, 1], [0, 0, 1, 2]])
continue
for j in range(i+1, m):
l_ji = a[j][i] / a[i][i]
a[j][i] = l_ji
for k in range(i+1, n):
a[j][k] -= l_ji * a[i][k]
return swaps
def ddm_ilu_solve(x, L, U, swaps, b):
"""x <-- solve(L*U*x = swaps(b))
Solve a linear system, $A*x = b$, given an LU factorization of $A$.
Uses division in the ground domain which must be a field.
Modifies $x$ in place.
Examples
========
Compute the LU decomposition of $A$ (in place):
>>> from sympy import QQ
>>> from sympy.polys.matrices.dense import ddm_ilu, ddm_ilu_solve
>>> A = [[QQ(1), QQ(2)], [QQ(3), QQ(4)]]
>>> swaps = ddm_ilu(A)
>>> A
[[1, 2], [3, -2]]
>>> L = U = A
Solve the linear system:
>>> b = [[QQ(5)], [QQ(6)]]
>>> x = [[None], [None]]
>>> ddm_ilu_solve(x, L, U, swaps, b)
>>> x
[[-4], [9/2]]
See Also
========
ddm_ilu
Compute the LU decomposition of a matrix in place.
ddm_ilu_split
Compute the LU decomposition of a matrix and separate $L$ and $U$.
sympy.polys.matrices.domainmatrix.DomainMatrix.lu_solve
Higher level interface to this function.
"""
m = len(U)
if not m:
return
n = len(U[0])
m2 = len(b)
if not m2:
raise DMShapeError("Shape mismtch")
o = len(b[0])
if m != m2:
raise DMShapeError("Shape mismtch")
if m < n:
raise NotImplementedError("Underdetermined")
if swaps:
b = [row[:] for row in b]
for i1, i2 in swaps:
b[i1], b[i2] = b[i2], b[i1]
# solve Ly = b
y = [[None] * o for _ in range(m)]
for k in range(o):
for i in range(m):
rhs = b[i][k]
for j in range(i):
rhs -= L[i][j] * y[j][k]
y[i][k] = rhs
if m > n:
for i in range(n, m):
for j in range(o):
if y[i][j]:
raise DMNonInvertibleMatrixError
# Solve Ux = y
for k in range(o):
for i in reversed(range(n)):
if not U[i][i]:
raise DMNonInvertibleMatrixError
rhs = y[i][k]
for j in range(i+1, n):
rhs -= U[i][j] * x[j][k]
x[i][k] = rhs / U[i][i]
def ddm_berk(M, K):
"""
Berkowitz algorithm for computing the characteristic polynomial.
Explanation
===========
The Berkowitz algorithm is a division-free algorithm for computing the
characteristic polynomial of a matrix over any commutative ring using only
arithmetic in the coefficient ring.
Examples
========
>>> from sympy import Matrix
>>> from sympy.polys.matrices.dense import ddm_berk
>>> from sympy.polys.domains import ZZ
>>> M = [[ZZ(1), ZZ(2)], [ZZ(3), ZZ(4)]]
>>> ddm_berk(M, ZZ)
[[1], [-5], [-2]]
>>> Matrix(M).charpoly()
PurePoly(lambda**2 - 5*lambda - 2, lambda, domain='ZZ')
See Also
========
sympy.polys.matrices.domainmatrix.DomainMatrix.charpoly
The high-level interface to this function.
References
==========
.. [1] https://en.wikipedia.org/wiki/Samuelson%E2%80%93Berkowitz_algorithm
"""
m = len(M)
if not m:
return [[K.one]]
n = len(M[0])
if m != n:
raise DMShapeError("Not square")
if n == 1:
return [[K.one], [-M[0][0]]]
a = M[0][0]
R = [M[0][1:]]
C = [[row[0]] for row in M[1:]]
A = [row[1:] for row in M[1:]]
q = ddm_berk(A, K)
T = [[K.zero] * n for _ in range(n+1)]
for i in range(n):
T[i][i] = K.one
T[i+1][i] = -a
for i in range(2, n+1):
if i == 2:
AnC = C
else:
C = AnC
AnC = [[K.zero] for row in C]
ddm_imatmul(AnC, A, C)
RAnC = [[K.zero]]
ddm_imatmul(RAnC, R, AnC)
for j in range(0, n+1-i):
T[i+j][j] = -RAnC[0][0]
qout = [[K.zero] for _ in range(n+1)]
ddm_imatmul(qout, T, q)
return qout

View File

@ -0,0 +1,35 @@
"""
sympy.polys.matrices.dfm
Provides the :class:`DFM` class if ``GROUND_TYPES=flint'``. Otherwise, ``DFM``
is a placeholder class that raises NotImplementedError when instantiated.
"""
from sympy.external.gmpy import GROUND_TYPES
if GROUND_TYPES == "flint": # pragma: no cover
# When python-flint is installed we will try to use it for dense matrices
# if the domain is supported by python-flint.
from ._dfm import DFM
else: # pragma: no cover
# Other code should be able to import this and it should just present as a
# version of DFM that does not support any domains.
class DFM_dummy:
"""
Placeholder class for DFM when python-flint is not installed.
"""
def __init__(*args, **kwargs):
raise NotImplementedError("DFM requires GROUND_TYPES=flint.")
@classmethod
def _supports_domain(cls, domain):
return False
@classmethod
def _get_flint_func(cls, domain):
raise NotImplementedError("DFM requires GROUND_TYPES=flint.")
# mypy really struggles with this kind of conditional type assignment.
# Maybe there is a better way to annotate this rather than type: ignore.
DFM = DFM_dummy # type: ignore

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,122 @@
"""
Module for the DomainScalar class.
A DomainScalar represents an element which is in a particular
Domain. The idea is that the DomainScalar class provides the
convenience routines for unifying elements with different domains.
It assists in Scalar Multiplication and getitem for DomainMatrix.
"""
from ..constructor import construct_domain
from sympy.polys.domains import Domain, ZZ
class DomainScalar:
r"""
docstring
"""
def __new__(cls, element, domain):
if not isinstance(domain, Domain):
raise TypeError("domain should be of type Domain")
if not domain.of_type(element):
raise TypeError("element %s should be in domain %s" % (element, domain))
return cls.new(element, domain)
@classmethod
def new(cls, element, domain):
obj = super().__new__(cls)
obj.element = element
obj.domain = domain
return obj
def __repr__(self):
return repr(self.element)
@classmethod
def from_sympy(cls, expr):
[domain, [element]] = construct_domain([expr])
return cls.new(element, domain)
def to_sympy(self):
return self.domain.to_sympy(self.element)
def to_domain(self, domain):
element = domain.convert_from(self.element, self.domain)
return self.new(element, domain)
def convert_to(self, domain):
return self.to_domain(domain)
def unify(self, other):
domain = self.domain.unify(other.domain)
return self.to_domain(domain), other.to_domain(domain)
def __bool__(self):
return bool(self.element)
def __add__(self, other):
if not isinstance(other, DomainScalar):
return NotImplemented
self, other = self.unify(other)
return self.new(self.element + other.element, self.domain)
def __sub__(self, other):
if not isinstance(other, DomainScalar):
return NotImplemented
self, other = self.unify(other)
return self.new(self.element - other.element, self.domain)
def __mul__(self, other):
if not isinstance(other, DomainScalar):
if isinstance(other, int):
other = DomainScalar(ZZ(other), ZZ)
else:
return NotImplemented
self, other = self.unify(other)
return self.new(self.element * other.element, self.domain)
def __floordiv__(self, other):
if not isinstance(other, DomainScalar):
return NotImplemented
self, other = self.unify(other)
return self.new(self.domain.quo(self.element, other.element), self.domain)
def __mod__(self, other):
if not isinstance(other, DomainScalar):
return NotImplemented
self, other = self.unify(other)
return self.new(self.domain.rem(self.element, other.element), self.domain)
def __divmod__(self, other):
if not isinstance(other, DomainScalar):
return NotImplemented
self, other = self.unify(other)
q, r = self.domain.div(self.element, other.element)
return (self.new(q, self.domain), self.new(r, self.domain))
def __pow__(self, n):
if not isinstance(n, int):
return NotImplemented
return self.new(self.element**n, self.domain)
def __pos__(self):
return self.new(+self.element, self.domain)
def __neg__(self):
return self.new(-self.element, self.domain)
def __eq__(self, other):
if not isinstance(other, DomainScalar):
return NotImplemented
return self.element == other.element and self.domain == other.domain
def is_zero(self):
return self.element == self.domain.zero
def is_one(self):
return self.element == self.domain.one

View File

@ -0,0 +1,90 @@
"""
Routines for computing eigenvectors with DomainMatrix.
"""
from sympy.core.symbol import Dummy
from ..agca.extensions import FiniteExtension
from ..factortools import dup_factor_list
from ..polyroots import roots
from ..polytools import Poly
from ..rootoftools import CRootOf
from .domainmatrix import DomainMatrix
def dom_eigenvects(A, l=Dummy('lambda')):
charpoly = A.charpoly()
rows, cols = A.shape
domain = A.domain
_, factors = dup_factor_list(charpoly, domain)
rational_eigenvects = []
algebraic_eigenvects = []
for base, exp in factors:
if len(base) == 2:
field = domain
eigenval = -base[1] / base[0]
EE_items = [
[eigenval if i == j else field.zero for j in range(cols)]
for i in range(rows)]
EE = DomainMatrix(EE_items, (rows, cols), field)
basis = (A - EE).nullspace(divide_last=True)
rational_eigenvects.append((field, eigenval, exp, basis))
else:
minpoly = Poly.from_list(base, l, domain=domain)
field = FiniteExtension(minpoly)
eigenval = field(l)
AA_items = [
[Poly.from_list([item], l, domain=domain).rep for item in row]
for row in A.rep.to_ddm()]
AA_items = [[field(item) for item in row] for row in AA_items]
AA = DomainMatrix(AA_items, (rows, cols), field)
EE_items = [
[eigenval if i == j else field.zero for j in range(cols)]
for i in range(rows)]
EE = DomainMatrix(EE_items, (rows, cols), field)
basis = (AA - EE).nullspace(divide_last=True)
algebraic_eigenvects.append((field, minpoly, exp, basis))
return rational_eigenvects, algebraic_eigenvects
def dom_eigenvects_to_sympy(
rational_eigenvects, algebraic_eigenvects,
Matrix, **kwargs
):
result = []
for field, eigenvalue, multiplicity, eigenvects in rational_eigenvects:
eigenvects = eigenvects.rep.to_ddm()
eigenvalue = field.to_sympy(eigenvalue)
new_eigenvects = [
Matrix([field.to_sympy(x) for x in vect])
for vect in eigenvects]
result.append((eigenvalue, multiplicity, new_eigenvects))
for field, minpoly, multiplicity, eigenvects in algebraic_eigenvects:
eigenvects = eigenvects.rep.to_ddm()
l = minpoly.gens[0]
eigenvects = [[field.to_sympy(x) for x in vect] for vect in eigenvects]
degree = minpoly.degree()
minpoly = minpoly.as_expr()
eigenvals = roots(minpoly, l, **kwargs)
if len(eigenvals) != degree:
eigenvals = [CRootOf(minpoly, l, idx) for idx in range(degree)]
for eigenvalue in eigenvals:
new_eigenvects = [
Matrix([x.subs(l, eigenvalue) for x in vect])
for vect in eigenvects]
result.append((eigenvalue, multiplicity, new_eigenvects))
return result

View File

@ -0,0 +1,67 @@
"""
Module to define exceptions to be used in sympy.polys.matrices modules and
classes.
Ideally all exceptions raised in these modules would be defined and documented
here and not e.g. imported from matrices. Also ideally generic exceptions like
ValueError/TypeError would not be raised anywhere.
"""
class DMError(Exception):
"""Base class for errors raised by DomainMatrix"""
pass
class DMBadInputError(DMError):
"""list of lists is inconsistent with shape"""
pass
class DMDomainError(DMError):
"""domains do not match"""
pass
class DMNotAField(DMDomainError):
"""domain is not a field"""
pass
class DMFormatError(DMError):
"""mixed dense/sparse not supported"""
pass
class DMNonInvertibleMatrixError(DMError):
"""The matrix in not invertible"""
pass
class DMRankError(DMError):
"""matrix does not have expected rank"""
pass
class DMShapeError(DMError):
"""shapes are inconsistent"""
pass
class DMNonSquareMatrixError(DMShapeError):
"""The matrix is not square"""
pass
class DMValueError(DMError):
"""The value passed is invalid"""
pass
__all__ = [
'DMError', 'DMBadInputError', 'DMDomainError', 'DMFormatError',
'DMRankError', 'DMShapeError', 'DMNotAField',
'DMNonInvertibleMatrixError', 'DMNonSquareMatrixError', 'DMValueError'
]

View File

@ -0,0 +1,230 @@
#
# sympy.polys.matrices.linsolve module
#
# This module defines the _linsolve function which is the internal workhorse
# used by linsolve. This computes the solution of a system of linear equations
# using the SDM sparse matrix implementation in sympy.polys.matrices.sdm. This
# is a replacement for solve_lin_sys in sympy.polys.solvers which is
# inefficient for large sparse systems due to the use of a PolyRing with many
# generators:
#
# https://github.com/sympy/sympy/issues/20857
#
# The implementation of _linsolve here handles:
#
# - Extracting the coefficients from the Expr/Eq input equations.
# - Constructing a domain and converting the coefficients to
# that domain.
# - Using the SDM.rref, SDM.nullspace etc methods to generate the full
# solution working with arithmetic only in the domain of the coefficients.
#
# The routines here are particularly designed to be efficient for large sparse
# systems of linear equations although as well as dense systems. It is
# possible that for some small dense systems solve_lin_sys which uses the
# dense matrix implementation DDM will be more efficient. With smaller systems
# though the bulk of the time is spent just preprocessing the inputs and the
# relative time spent in rref is too small to be noticeable.
#
from collections import defaultdict
from sympy.core.add import Add
from sympy.core.mul import Mul
from sympy.core.singleton import S
from sympy.polys.constructor import construct_domain
from sympy.polys.solvers import PolyNonlinearError
from .sdm import (
SDM,
sdm_irref,
sdm_particular_from_rref,
sdm_nullspace_from_rref
)
from sympy.utilities.misc import filldedent
def _linsolve(eqs, syms):
"""Solve a linear system of equations.
Examples
========
Solve a linear system with a unique solution:
>>> from sympy import symbols, Eq
>>> from sympy.polys.matrices.linsolve import _linsolve
>>> x, y = symbols('x, y')
>>> eqs = [Eq(x + y, 1), Eq(x - y, 2)]
>>> _linsolve(eqs, [x, y])
{x: 3/2, y: -1/2}
In the case of underdetermined systems the solution will be expressed in
terms of the unknown symbols that are unconstrained:
>>> _linsolve([Eq(x + y, 0)], [x, y])
{x: -y, y: y}
"""
# Number of unknowns (columns in the non-augmented matrix)
nsyms = len(syms)
# Convert to sparse augmented matrix (len(eqs) x (nsyms+1))
eqsdict, const = _linear_eq_to_dict(eqs, syms)
Aaug = sympy_dict_to_dm(eqsdict, const, syms)
K = Aaug.domain
# sdm_irref has issues with float matrices. This uses the ddm_rref()
# function. When sdm_rref() can handle float matrices reasonably this
# should be removed...
if K.is_RealField or K.is_ComplexField:
Aaug = Aaug.to_ddm().rref()[0].to_sdm()
# Compute reduced-row echelon form (RREF)
Arref, pivots, nzcols = sdm_irref(Aaug)
# No solution:
if pivots and pivots[-1] == nsyms:
return None
# Particular solution for non-homogeneous system:
P = sdm_particular_from_rref(Arref, nsyms+1, pivots)
# Nullspace - general solution to homogeneous system
# Note: using nsyms not nsyms+1 to ignore last column
V, nonpivots = sdm_nullspace_from_rref(Arref, K.one, nsyms, pivots, nzcols)
# Collect together terms from particular and nullspace:
sol = defaultdict(list)
for i, v in P.items():
sol[syms[i]].append(K.to_sympy(v))
for npi, Vi in zip(nonpivots, V):
sym = syms[npi]
for i, v in Vi.items():
sol[syms[i]].append(sym * K.to_sympy(v))
# Use a single call to Add for each term:
sol = {s: Add(*terms) for s, terms in sol.items()}
# Fill in the zeros:
zero = S.Zero
for s in set(syms) - set(sol):
sol[s] = zero
# All done!
return sol
def sympy_dict_to_dm(eqs_coeffs, eqs_rhs, syms):
"""Convert a system of dict equations to a sparse augmented matrix"""
elems = set(eqs_rhs).union(*(e.values() for e in eqs_coeffs))
K, elems_K = construct_domain(elems, field=True, extension=True)
elem_map = dict(zip(elems, elems_K))
neqs = len(eqs_coeffs)
nsyms = len(syms)
sym2index = dict(zip(syms, range(nsyms)))
eqsdict = []
for eq, rhs in zip(eqs_coeffs, eqs_rhs):
eqdict = {sym2index[s]: elem_map[c] for s, c in eq.items()}
if rhs:
eqdict[nsyms] = -elem_map[rhs]
if eqdict:
eqsdict.append(eqdict)
sdm_aug = SDM(enumerate(eqsdict), (neqs, nsyms + 1), K)
return sdm_aug
def _linear_eq_to_dict(eqs, syms):
"""Convert a system Expr/Eq equations into dict form, returning
the coefficient dictionaries and a list of syms-independent terms
from each expression in ``eqs```.
Examples
========
>>> from sympy.polys.matrices.linsolve import _linear_eq_to_dict
>>> from sympy.abc import x
>>> _linear_eq_to_dict([2*x + 3], {x})
([{x: 2}], [3])
"""
coeffs = []
ind = []
symset = set(syms)
for e in eqs:
if e.is_Equality:
coeff, terms = _lin_eq2dict(e.lhs, symset)
cR, tR = _lin_eq2dict(e.rhs, symset)
# there were no nonlinear errors so now
# cancellation is allowed
coeff -= cR
for k, v in tR.items():
if k in terms:
terms[k] -= v
else:
terms[k] = -v
# don't store coefficients of 0, however
terms = {k: v for k, v in terms.items() if v}
c, d = coeff, terms
else:
c, d = _lin_eq2dict(e, symset)
coeffs.append(d)
ind.append(c)
return coeffs, ind
def _lin_eq2dict(a, symset):
"""return (c, d) where c is the sym-independent part of ``a`` and
``d`` is an efficiently calculated dictionary mapping symbols to
their coefficients. A PolyNonlinearError is raised if non-linearity
is detected.
The values in the dictionary will be non-zero.
Examples
========
>>> from sympy.polys.matrices.linsolve import _lin_eq2dict
>>> from sympy.abc import x, y
>>> _lin_eq2dict(x + 2*y + 3, {x, y})
(3, {x: 1, y: 2})
"""
if a in symset:
return S.Zero, {a: S.One}
elif a.is_Add:
terms_list = defaultdict(list)
coeff_list = []
for ai in a.args:
ci, ti = _lin_eq2dict(ai, symset)
coeff_list.append(ci)
for mij, cij in ti.items():
terms_list[mij].append(cij)
coeff = Add(*coeff_list)
terms = {sym: Add(*coeffs) for sym, coeffs in terms_list.items()}
return coeff, terms
elif a.is_Mul:
terms = terms_coeff = None
coeff_list = []
for ai in a.args:
ci, ti = _lin_eq2dict(ai, symset)
if not ti:
coeff_list.append(ci)
elif terms is None:
terms = ti
terms_coeff = ci
else:
# since ti is not null and we already have
# a term, this is a cross term
raise PolyNonlinearError(filldedent('''
nonlinear cross-term: %s''' % a))
coeff = Mul._from_args(coeff_list)
if terms is None:
return coeff, {}
else:
terms = {sym: coeff * c for sym, c in terms.items()}
return coeff * terms_coeff, terms
elif not a.has_xfree(symset):
return a, {}
else:
raise PolyNonlinearError('nonlinear term: %s' % a)

View File

@ -0,0 +1,94 @@
from __future__ import annotations
from math import floor as mfloor
from sympy.polys.domains import ZZ, QQ
from sympy.polys.matrices.exceptions import DMRankError, DMShapeError, DMValueError, DMDomainError
def _ddm_lll(x, delta=QQ(3, 4), return_transform=False):
if QQ(1, 4) >= delta or delta >= QQ(1, 1):
raise DMValueError("delta must lie in range (0.25, 1)")
if x.shape[0] > x.shape[1]:
raise DMShapeError("input matrix must have shape (m, n) with m <= n")
if x.domain != ZZ:
raise DMDomainError("input matrix domain must be ZZ")
m = x.shape[0]
n = x.shape[1]
k = 1
y = x.copy()
y_star = x.zeros((m, n), QQ)
mu = x.zeros((m, m), QQ)
g_star = [QQ(0, 1) for _ in range(m)]
half = QQ(1, 2)
T = x.eye(m, ZZ) if return_transform else None
linear_dependent_error = "input matrix contains linearly dependent rows"
def closest_integer(x):
return ZZ(mfloor(x + half))
def lovasz_condition(k: int) -> bool:
return g_star[k] >= ((delta - mu[k][k - 1] ** 2) * g_star[k - 1])
def mu_small(k: int, j: int) -> bool:
return abs(mu[k][j]) <= half
def dot_rows(x, y, rows: tuple[int, int]):
return sum(x[rows[0]][z] * y[rows[1]][z] for z in range(x.shape[1]))
def reduce_row(T, mu, y, rows: tuple[int, int]):
r = closest_integer(mu[rows[0]][rows[1]])
y[rows[0]] = [y[rows[0]][z] - r * y[rows[1]][z] for z in range(n)]
mu[rows[0]][:rows[1]] = [mu[rows[0]][z] - r * mu[rows[1]][z] for z in range(rows[1])]
mu[rows[0]][rows[1]] -= r
if return_transform:
T[rows[0]] = [T[rows[0]][z] - r * T[rows[1]][z] for z in range(m)]
for i in range(m):
y_star[i] = [QQ.convert_from(z, ZZ) for z in y[i]]
for j in range(i):
row_dot = dot_rows(y, y_star, (i, j))
try:
mu[i][j] = row_dot / g_star[j]
except ZeroDivisionError:
raise DMRankError(linear_dependent_error)
y_star[i] = [y_star[i][z] - mu[i][j] * y_star[j][z] for z in range(n)]
g_star[i] = dot_rows(y_star, y_star, (i, i))
while k < m:
if not mu_small(k, k - 1):
reduce_row(T, mu, y, (k, k - 1))
if lovasz_condition(k):
for l in range(k - 2, -1, -1):
if not mu_small(k, l):
reduce_row(T, mu, y, (k, l))
k += 1
else:
nu = mu[k][k - 1]
alpha = g_star[k] + nu ** 2 * g_star[k - 1]
try:
beta = g_star[k - 1] / alpha
except ZeroDivisionError:
raise DMRankError(linear_dependent_error)
mu[k][k - 1] = nu * beta
g_star[k] = g_star[k] * beta
g_star[k - 1] = alpha
y[k], y[k - 1] = y[k - 1], y[k]
mu[k][:k - 1], mu[k - 1][:k - 1] = mu[k - 1][:k - 1], mu[k][:k - 1]
for i in range(k + 1, m):
xi = mu[i][k]
mu[i][k] = mu[i][k - 1] - nu * xi
mu[i][k - 1] = mu[k][k - 1] * mu[i][k] + xi
if return_transform:
T[k], T[k - 1] = T[k - 1], T[k]
k = max(k - 1, 1)
assert all(lovasz_condition(i) for i in range(1, m))
assert all(mu_small(i, j) for i in range(m) for j in range(i))
return y, T
def ddm_lll(x, delta=QQ(3, 4)):
return _ddm_lll(x, delta=delta, return_transform=False)[0]
def ddm_lll_transform(x, delta=QQ(3, 4)):
return _ddm_lll(x, delta=delta, return_transform=True)

View File

@ -0,0 +1,406 @@
'''Functions returning normal forms of matrices'''
from collections import defaultdict
from .domainmatrix import DomainMatrix
from .exceptions import DMDomainError, DMShapeError
from sympy.ntheory.modular import symmetric_residue
from sympy.polys.domains import QQ, ZZ
# TODO (future work):
# There are faster algorithms for Smith and Hermite normal forms, which
# we should implement. See e.g. the Kannan-Bachem algorithm:
# <https://www.researchgate.net/publication/220617516_Polynomial_Algorithms_for_Computing_the_Smith_and_Hermite_Normal_Forms_of_an_Integer_Matrix>
def smith_normal_form(m):
'''
Return the Smith Normal Form of a matrix `m` over the ring `domain`.
This will only work if the ring is a principal ideal domain.
Examples
========
>>> from sympy import ZZ
>>> from sympy.polys.matrices import DomainMatrix
>>> from sympy.polys.matrices.normalforms import smith_normal_form
>>> m = DomainMatrix([[ZZ(12), ZZ(6), ZZ(4)],
... [ZZ(3), ZZ(9), ZZ(6)],
... [ZZ(2), ZZ(16), ZZ(14)]], (3, 3), ZZ)
>>> print(smith_normal_form(m).to_Matrix())
Matrix([[1, 0, 0], [0, 10, 0], [0, 0, -30]])
'''
invs = invariant_factors(m)
smf = DomainMatrix.diag(invs, m.domain, m.shape)
return smf
def add_columns(m, i, j, a, b, c, d):
# replace m[:, i] by a*m[:, i] + b*m[:, j]
# and m[:, j] by c*m[:, i] + d*m[:, j]
for k in range(len(m)):
e = m[k][i]
m[k][i] = a*e + b*m[k][j]
m[k][j] = c*e + d*m[k][j]
def invariant_factors(m):
'''
Return the tuple of abelian invariants for a matrix `m`
(as in the Smith-Normal form)
References
==========
[1] https://en.wikipedia.org/wiki/Smith_normal_form#Algorithm
[2] https://web.archive.org/web/20200331143852/https://sierra.nmsu.edu/morandi/notes/SmithNormalForm.pdf
'''
domain = m.domain
if not domain.is_PID:
msg = "The matrix entries must be over a principal ideal domain"
raise ValueError(msg)
if 0 in m.shape:
return ()
rows, cols = shape = m.shape
m = list(m.to_dense().rep.to_ddm())
def add_rows(m, i, j, a, b, c, d):
# replace m[i, :] by a*m[i, :] + b*m[j, :]
# and m[j, :] by c*m[i, :] + d*m[j, :]
for k in range(cols):
e = m[i][k]
m[i][k] = a*e + b*m[j][k]
m[j][k] = c*e + d*m[j][k]
def clear_column(m):
# make m[1:, 0] zero by row and column operations
if m[0][0] == 0:
return m # pragma: nocover
pivot = m[0][0]
for j in range(1, rows):
if m[j][0] == 0:
continue
d, r = domain.div(m[j][0], pivot)
if r == 0:
add_rows(m, 0, j, 1, 0, -d, 1)
else:
a, b, g = domain.gcdex(pivot, m[j][0])
d_0 = domain.div(m[j][0], g)[0]
d_j = domain.div(pivot, g)[0]
add_rows(m, 0, j, a, b, d_0, -d_j)
pivot = g
return m
def clear_row(m):
# make m[0, 1:] zero by row and column operations
if m[0][0] == 0:
return m # pragma: nocover
pivot = m[0][0]
for j in range(1, cols):
if m[0][j] == 0:
continue
d, r = domain.div(m[0][j], pivot)
if r == 0:
add_columns(m, 0, j, 1, 0, -d, 1)
else:
a, b, g = domain.gcdex(pivot, m[0][j])
d_0 = domain.div(m[0][j], g)[0]
d_j = domain.div(pivot, g)[0]
add_columns(m, 0, j, a, b, d_0, -d_j)
pivot = g
return m
# permute the rows and columns until m[0,0] is non-zero if possible
ind = [i for i in range(rows) if m[i][0] != 0]
if ind and ind[0] != 0:
m[0], m[ind[0]] = m[ind[0]], m[0]
else:
ind = [j for j in range(cols) if m[0][j] != 0]
if ind and ind[0] != 0:
for row in m:
row[0], row[ind[0]] = row[ind[0]], row[0]
# make the first row and column except m[0,0] zero
while (any(m[0][i] != 0 for i in range(1,cols)) or
any(m[i][0] != 0 for i in range(1,rows))):
m = clear_column(m)
m = clear_row(m)
if 1 in shape:
invs = ()
else:
lower_right = DomainMatrix([r[1:] for r in m[1:]], (rows-1, cols-1), domain)
invs = invariant_factors(lower_right)
if m[0][0]:
result = [m[0][0]]
result.extend(invs)
# in case m[0] doesn't divide the invariants of the rest of the matrix
for i in range(len(result)-1):
if result[i] and domain.div(result[i+1], result[i])[1] != 0:
g = domain.gcd(result[i+1], result[i])
result[i+1] = domain.div(result[i], g)[0]*result[i+1]
result[i] = g
else:
break
else:
result = invs + (m[0][0],)
return tuple(result)
def _gcdex(a, b):
r"""
This supports the functions that compute Hermite Normal Form.
Explanation
===========
Let x, y be the coefficients returned by the extended Euclidean
Algorithm, so that x*a + y*b = g. In the algorithms for computing HNF,
it is critical that x, y not only satisfy the condition of being small
in magnitude -- namely that |x| <= |b|/g, |y| <- |a|/g -- but also that
y == 0 when a | b.
"""
x, y, g = ZZ.gcdex(a, b)
if a != 0 and b % a == 0:
y = 0
x = -1 if a < 0 else 1
return x, y, g
def _hermite_normal_form(A):
r"""
Compute the Hermite Normal Form of DomainMatrix *A* over :ref:`ZZ`.
Parameters
==========
A : :py:class:`~.DomainMatrix` over domain :ref:`ZZ`.
Returns
=======
:py:class:`~.DomainMatrix`
The HNF of matrix *A*.
Raises
======
DMDomainError
If the domain of the matrix is not :ref:`ZZ`.
References
==========
.. [1] Cohen, H. *A Course in Computational Algebraic Number Theory.*
(See Algorithm 2.4.5.)
"""
if not A.domain.is_ZZ:
raise DMDomainError('Matrix must be over domain ZZ.')
# We work one row at a time, starting from the bottom row, and working our
# way up.
m, n = A.shape
A = A.to_dense().rep.to_ddm().copy()
# Our goal is to put pivot entries in the rightmost columns.
# Invariant: Before processing each row, k should be the index of the
# leftmost column in which we have so far put a pivot.
k = n
for i in range(m - 1, -1, -1):
if k == 0:
# This case can arise when n < m and we've already found n pivots.
# We don't need to consider any more rows, because this is already
# the maximum possible number of pivots.
break
k -= 1
# k now points to the column in which we want to put a pivot.
# We want zeros in all entries to the left of the pivot column.
for j in range(k - 1, -1, -1):
if A[i][j] != 0:
# Replace cols j, k by lin combs of these cols such that, in row i,
# col j has 0, while col k has the gcd of their row i entries. Note
# that this ensures a nonzero entry in col k.
u, v, d = _gcdex(A[i][k], A[i][j])
r, s = A[i][k] // d, A[i][j] // d
add_columns(A, k, j, u, v, -s, r)
b = A[i][k]
# Do not want the pivot entry to be negative.
if b < 0:
add_columns(A, k, k, -1, 0, -1, 0)
b = -b
# The pivot entry will be 0 iff the row was 0 from the pivot col all the
# way to the left. In this case, we are still working on the same pivot
# col for the next row. Therefore:
if b == 0:
k += 1
# If the pivot entry is nonzero, then we want to reduce all entries to its
# right in the sense of the division algorithm, i.e. make them all remainders
# w.r.t. the pivot as divisor.
else:
for j in range(k + 1, n):
q = A[i][j] // b
add_columns(A, j, k, 1, -q, 0, 1)
# Finally, the HNF consists of those columns of A in which we succeeded in making
# a nonzero pivot.
return DomainMatrix.from_rep(A.to_dfm_or_ddm())[:, k:]
def _hermite_normal_form_modulo_D(A, D):
r"""
Perform the mod *D* Hermite Normal Form reduction algorithm on
:py:class:`~.DomainMatrix` *A*.
Explanation
===========
If *A* is an $m \times n$ matrix of rank $m$, having Hermite Normal Form
$W$, and if *D* is any positive integer known in advance to be a multiple
of $\det(W)$, then the HNF of *A* can be computed by an algorithm that
works mod *D* in order to prevent coefficient explosion.
Parameters
==========
A : :py:class:`~.DomainMatrix` over :ref:`ZZ`
$m \times n$ matrix, having rank $m$.
D : :ref:`ZZ`
Positive integer, known to be a multiple of the determinant of the
HNF of *A*.
Returns
=======
:py:class:`~.DomainMatrix`
The HNF of matrix *A*.
Raises
======
DMDomainError
If the domain of the matrix is not :ref:`ZZ`, or
if *D* is given but is not in :ref:`ZZ`.
DMShapeError
If the matrix has more rows than columns.
References
==========
.. [1] Cohen, H. *A Course in Computational Algebraic Number Theory.*
(See Algorithm 2.4.8.)
"""
if not A.domain.is_ZZ:
raise DMDomainError('Matrix must be over domain ZZ.')
if not ZZ.of_type(D) or D < 1:
raise DMDomainError('Modulus D must be positive element of domain ZZ.')
def add_columns_mod_R(m, R, i, j, a, b, c, d):
# replace m[:, i] by (a*m[:, i] + b*m[:, j]) % R
# and m[:, j] by (c*m[:, i] + d*m[:, j]) % R
for k in range(len(m)):
e = m[k][i]
m[k][i] = symmetric_residue((a * e + b * m[k][j]) % R, R)
m[k][j] = symmetric_residue((c * e + d * m[k][j]) % R, R)
W = defaultdict(dict)
m, n = A.shape
if n < m:
raise DMShapeError('Matrix must have at least as many columns as rows.')
A = A.to_dense().rep.to_ddm().copy()
k = n
R = D
for i in range(m - 1, -1, -1):
k -= 1
for j in range(k - 1, -1, -1):
if A[i][j] != 0:
u, v, d = _gcdex(A[i][k], A[i][j])
r, s = A[i][k] // d, A[i][j] // d
add_columns_mod_R(A, R, k, j, u, v, -s, r)
b = A[i][k]
if b == 0:
A[i][k] = b = R
u, v, d = _gcdex(b, R)
for ii in range(m):
W[ii][i] = u*A[ii][k] % R
if W[i][i] == 0:
W[i][i] = R
for j in range(i + 1, m):
q = W[i][j] // W[i][i]
add_columns(W, j, i, 1, -q, 0, 1)
R //= d
return DomainMatrix(W, (m, m), ZZ).to_dense()
def hermite_normal_form(A, *, D=None, check_rank=False):
r"""
Compute the Hermite Normal Form of :py:class:`~.DomainMatrix` *A* over
:ref:`ZZ`.
Examples
========
>>> from sympy import ZZ
>>> from sympy.polys.matrices import DomainMatrix
>>> from sympy.polys.matrices.normalforms import hermite_normal_form
>>> m = DomainMatrix([[ZZ(12), ZZ(6), ZZ(4)],
... [ZZ(3), ZZ(9), ZZ(6)],
... [ZZ(2), ZZ(16), ZZ(14)]], (3, 3), ZZ)
>>> print(hermite_normal_form(m).to_Matrix())
Matrix([[10, 0, 2], [0, 15, 3], [0, 0, 2]])
Parameters
==========
A : $m \times n$ ``DomainMatrix`` over :ref:`ZZ`.
D : :ref:`ZZ`, optional
Let $W$ be the HNF of *A*. If known in advance, a positive integer *D*
being any multiple of $\det(W)$ may be provided. In this case, if *A*
also has rank $m$, then we may use an alternative algorithm that works
mod *D* in order to prevent coefficient explosion.
check_rank : boolean, optional (default=False)
The basic assumption is that, if you pass a value for *D*, then
you already believe that *A* has rank $m$, so we do not waste time
checking it for you. If you do want this to be checked (and the
ordinary, non-modulo *D* algorithm to be used if the check fails), then
set *check_rank* to ``True``.
Returns
=======
:py:class:`~.DomainMatrix`
The HNF of matrix *A*.
Raises
======
DMDomainError
If the domain of the matrix is not :ref:`ZZ`, or
if *D* is given but is not in :ref:`ZZ`.
DMShapeError
If the mod *D* algorithm is used but the matrix has more rows than
columns.
References
==========
.. [1] Cohen, H. *A Course in Computational Algebraic Number Theory.*
(See Algorithms 2.4.5 and 2.4.8.)
"""
if not A.domain.is_ZZ:
raise DMDomainError('Matrix must be over domain ZZ.')
if D is not None and (not check_rank or A.convert_to(QQ).rank() == A.shape[0]):
return _hermite_normal_form_modulo_D(A, D)
else:
return _hermite_normal_form(A)

View File

@ -0,0 +1,422 @@
# Algorithms for computing the reduced row echelon form of a matrix.
#
# We need to choose carefully which algorithms to use depending on the domain,
# shape, and sparsity of the matrix as well as things like the bit count in the
# case of ZZ or QQ. This is important because the algorithms have different
# performance characteristics in the extremes of dense vs sparse.
#
# In all cases we use the sparse implementations but we need to choose between
# Gauss-Jordan elimination with division and fraction-free Gauss-Jordan
# elimination. For very sparse matrices over ZZ with low bit counts it is
# asymptotically faster to use Gauss-Jordan elimination with division. For
# dense matrices with high bit counts it is asymptotically faster to use
# fraction-free Gauss-Jordan.
#
# The most important thing is to get the extreme cases right because it can
# make a big difference. In between the extremes though we have to make a
# choice and here we use empirically determined thresholds based on timings
# with random sparse matrices.
#
# In the case of QQ we have to consider the denominators as well. If the
# denominators are small then it is faster to clear them and use fraction-free
# Gauss-Jordan over ZZ. If the denominators are large then it is faster to use
# Gauss-Jordan elimination with division over QQ.
#
# Timings for the various algorithms can be found at
#
# https://github.com/sympy/sympy/issues/25410
# https://github.com/sympy/sympy/pull/25443
from sympy.polys.domains import ZZ
from sympy.polys.matrices.sdm import SDM, sdm_irref, sdm_rref_den
from sympy.polys.matrices.ddm import DDM
from sympy.polys.matrices.dense import ddm_irref, ddm_irref_den
def _dm_rref(M, *, method='auto'):
"""
Compute the reduced row echelon form of a ``DomainMatrix``.
This function is the implementation of :meth:`DomainMatrix.rref`.
Chooses the best algorithm depending on the domain, shape, and sparsity of
the matrix as well as things like the bit count in the case of :ref:`ZZ` or
:ref:`QQ`. The result is returned over the field associated with the domain
of the Matrix.
See Also
========
sympy.polys.matrices.domainmatrix.DomainMatrix.rref
The ``DomainMatrix`` method that calls this function.
sympy.polys.matrices.rref._dm_rref_den
Alternative function for computing RREF with denominator.
"""
method, use_fmt = _dm_rref_choose_method(M, method, denominator=False)
M, old_fmt = _dm_to_fmt(M, use_fmt)
if method == 'GJ':
# Use Gauss-Jordan with division over the associated field.
Mf = _to_field(M)
M_rref, pivots = _dm_rref_GJ(Mf)
elif method == 'FF':
# Use fraction-free GJ over the current domain.
M_rref_f, den, pivots = _dm_rref_den_FF(M)
M_rref = _to_field(M_rref_f) / den
elif method == 'CD':
# Clear denominators and use fraction-free GJ in the associated ring.
_, Mr = M.clear_denoms_rowwise(convert=True)
M_rref_f, den, pivots = _dm_rref_den_FF(Mr)
M_rref = _to_field(M_rref_f) / den
else:
raise ValueError(f"Unknown method for rref: {method}")
M_rref, _ = _dm_to_fmt(M_rref, old_fmt)
# Invariants:
# - M_rref is in the same format (sparse or dense) as the input matrix.
# - M_rref is in the associated field domain and any denominator was
# divided in (so is implicitly 1 now).
return M_rref, pivots
def _dm_rref_den(M, *, keep_domain=True, method='auto'):
"""
Compute the reduced row echelon form of a ``DomainMatrix`` with denominator.
This function is the implementation of :meth:`DomainMatrix.rref_den`.
Chooses the best algorithm depending on the domain, shape, and sparsity of
the matrix as well as things like the bit count in the case of :ref:`ZZ` or
:ref:`QQ`. The result is returned over the same domain as the input matrix
unless ``keep_domain=False`` in which case the result might be over an
associated ring or field domain.
See Also
========
sympy.polys.matrices.domainmatrix.DomainMatrix.rref_den
The ``DomainMatrix`` method that calls this function.
sympy.polys.matrices.rref._dm_rref
Alternative function for computing RREF without denominator.
"""
method, use_fmt = _dm_rref_choose_method(M, method, denominator=True)
M, old_fmt = _dm_to_fmt(M, use_fmt)
if method == 'FF':
# Use fraction-free GJ over the current domain.
M_rref, den, pivots = _dm_rref_den_FF(M)
elif method == 'GJ':
# Use Gauss-Jordan with division over the associated field.
M_rref_f, pivots = _dm_rref_GJ(_to_field(M))
# Convert back to the ring?
if keep_domain and M_rref_f.domain != M.domain:
_, M_rref = M_rref_f.clear_denoms(convert=True)
if pivots:
den = M_rref[0, pivots[0]].element
else:
den = M_rref.domain.one
else:
# Possibly an associated field
M_rref = M_rref_f
den = M_rref.domain.one
elif method == 'CD':
# Clear denominators and use fraction-free GJ in the associated ring.
_, Mr = M.clear_denoms_rowwise(convert=True)
M_rref_r, den, pivots = _dm_rref_den_FF(Mr)
if keep_domain and M_rref_r.domain != M.domain:
# Convert back to the field
M_rref = _to_field(M_rref_r) / den
den = M.domain.one
else:
# Possibly an associated ring
M_rref = M_rref_r
if pivots:
den = M_rref[0, pivots[0]].element
else:
den = M_rref.domain.one
else:
raise ValueError(f"Unknown method for rref: {method}")
M_rref, _ = _dm_to_fmt(M_rref, old_fmt)
# Invariants:
# - M_rref is in the same format (sparse or dense) as the input matrix.
# - If keep_domain=True then M_rref and den are in the same domain as the
# input matrix
# - If keep_domain=False then M_rref might be in an associated ring or
# field domain but den is always in the same domain as M_rref.
return M_rref, den, pivots
def _dm_to_fmt(M, fmt):
"""Convert a matrix to the given format and return the old format."""
old_fmt = M.rep.fmt
if old_fmt == fmt:
pass
elif fmt == 'dense':
M = M.to_dense()
elif fmt == 'sparse':
M = M.to_sparse()
else:
raise ValueError(f'Unknown format: {fmt}') # pragma: no cover
return M, old_fmt
# These are the four basic implementations that we want to choose between:
def _dm_rref_GJ(M):
"""Compute RREF using Gauss-Jordan elimination with division."""
if M.rep.fmt == 'sparse':
return _dm_rref_GJ_sparse(M)
else:
return _dm_rref_GJ_dense(M)
def _dm_rref_den_FF(M):
"""Compute RREF using fraction-free Gauss-Jordan elimination."""
if M.rep.fmt == 'sparse':
return _dm_rref_den_FF_sparse(M)
else:
return _dm_rref_den_FF_dense(M)
def _dm_rref_GJ_sparse(M):
"""Compute RREF using sparse Gauss-Jordan elimination with division."""
M_rref_d, pivots, _ = sdm_irref(M.rep)
M_rref_sdm = SDM(M_rref_d, M.shape, M.domain)
pivots = tuple(pivots)
return M.from_rep(M_rref_sdm), pivots
def _dm_rref_GJ_dense(M):
"""Compute RREF using dense Gauss-Jordan elimination with division."""
partial_pivot = M.domain.is_RR or M.domain.is_CC
ddm = M.rep.to_ddm().copy()
pivots = ddm_irref(ddm, _partial_pivot=partial_pivot)
M_rref_ddm = DDM(ddm, M.shape, M.domain)
pivots = tuple(pivots)
return M.from_rep(M_rref_ddm.to_dfm_or_ddm()), pivots
def _dm_rref_den_FF_sparse(M):
"""Compute RREF using sparse fraction-free Gauss-Jordan elimination."""
M_rref_d, den, pivots = sdm_rref_den(M.rep, M.domain)
M_rref_sdm = SDM(M_rref_d, M.shape, M.domain)
pivots = tuple(pivots)
return M.from_rep(M_rref_sdm), den, pivots
def _dm_rref_den_FF_dense(M):
"""Compute RREF using sparse fraction-free Gauss-Jordan elimination."""
ddm = M.rep.to_ddm().copy()
den, pivots = ddm_irref_den(ddm, M.domain)
M_rref_ddm = DDM(ddm, M.shape, M.domain)
pivots = tuple(pivots)
return M.from_rep(M_rref_ddm.to_dfm_or_ddm()), den, pivots
def _dm_rref_choose_method(M, method, *, denominator=False):
"""Choose the fastest method for computing RREF for M."""
if method != 'auto':
if method.endswith('_dense'):
method = method[:-len('_dense')]
use_fmt = 'dense'
else:
use_fmt = 'sparse'
else:
# The sparse implementations are always faster
use_fmt = 'sparse'
K = M.domain
if K.is_ZZ:
method = _dm_rref_choose_method_ZZ(M, denominator=denominator)
elif K.is_QQ:
method = _dm_rref_choose_method_QQ(M, denominator=denominator)
elif K.is_RR or K.is_CC:
# TODO: Add partial pivot support to the sparse implementations.
method = 'GJ'
use_fmt = 'dense'
elif K.is_EX and M.rep.fmt == 'dense' and not denominator:
# Do not switch to the sparse implementation for EX because the
# domain does not have proper canonicalization and the sparse
# implementation gives equivalent but non-identical results over EX
# from performing arithmetic in a different order. Specifically
# test_issue_23718 ends up getting a more complicated expression
# when using the sparse implementation. Probably the best fix for
# this is something else but for now we stick with the dense
# implementation for EX if the matrix is already dense.
method = 'GJ'
use_fmt = 'dense'
else:
# This is definitely suboptimal. More work is needed to determine
# the best method for computing RREF over different domains.
if denominator:
method = 'FF'
else:
method = 'GJ'
return method, use_fmt
def _dm_rref_choose_method_QQ(M, *, denominator=False):
"""Choose the fastest method for computing RREF over QQ."""
# The same sorts of considerations apply here as in the case of ZZ. Here
# though a new more significant consideration is what sort of denominators
# we have and what to do with them so we focus on that.
# First compute the density. This is the average number of non-zero entries
# per row but only counting rows that have at least one non-zero entry
# since RREF can ignore fully zero rows.
density, _, ncols = _dm_row_density(M)
# For sparse matrices use Gauss-Jordan elimination over QQ regardless.
if density < min(5, ncols/2):
return 'GJ'
# Compare the bit-length of the lcm of the denominators to the bit length
# of the numerators.
#
# The threshold here is empirical: we prefer rref over QQ if clearing
# denominators would result in a numerator matrix having 5x the bit size of
# the current numerators.
numers, denoms = _dm_QQ_numers_denoms(M)
numer_bits = max([n.bit_length() for n in numers], default=1)
denom_lcm = ZZ.one
for d in denoms:
denom_lcm = ZZ.lcm(denom_lcm, d)
if denom_lcm.bit_length() > 5*numer_bits:
return 'GJ'
# If we get here then the matrix is dense and the lcm of the denominators
# is not too large compared to the numerators. For particularly small
# denominators it is fastest just to clear them and use fraction-free
# Gauss-Jordan over ZZ. With very small denominators this is a little
# faster than using rref_den over QQ but there is an intermediate regime
# where rref_den over QQ is significantly faster. The small denominator
# case is probably very common because small fractions like 1/2 or 1/3 are
# often seen in user inputs.
if denom_lcm.bit_length() < 50:
return 'CD'
else:
return 'FF'
def _dm_rref_choose_method_ZZ(M, *, denominator=False):
"""Choose the fastest method for computing RREF over ZZ."""
# In the extreme of very sparse matrices and low bit counts it is faster to
# use Gauss-Jordan elimination over QQ rather than fraction-free
# Gauss-Jordan over ZZ. In the opposite extreme of dense matrices and high
# bit counts it is faster to use fraction-free Gauss-Jordan over ZZ. These
# two extreme cases need to be handled differently because they lead to
# different asymptotic complexities. In between these two extremes we need
# a threshold for deciding which method to use. This threshold is
# determined empirically by timing the two methods with random matrices.
# The disadvantage of using empirical timings is that future optimisations
# might change the relative speeds so this can easily become out of date.
# The main thing is to get the asymptotic complexity right for the extreme
# cases though so the precise value of the threshold is hopefully not too
# important.
# Empirically determined parameter.
PARAM = 10000
# First compute the density. This is the average number of non-zero entries
# per row but only counting rows that have at least one non-zero entry
# since RREF can ignore fully zero rows.
density, nrows_nz, ncols = _dm_row_density(M)
# For small matrices use QQ if more than half the entries are zero.
if nrows_nz < 10:
if density < ncols/2:
return 'GJ'
else:
return 'FF'
# These are just shortcuts for the formula below.
if density < 5:
return 'GJ'
elif density > 5 + PARAM/nrows_nz:
return 'FF' # pragma: no cover
# Maximum bitsize of any entry.
elements = _dm_elements(M)
bits = max([e.bit_length() for e in elements], default=1)
# Wideness parameter. This is 1 for square or tall matrices but >1 for wide
# matrices.
wideness = max(1, 2/3*ncols/nrows_nz)
max_density = (5 + PARAM/(nrows_nz*bits**2)) * wideness
if density < max_density:
return 'GJ'
else:
return 'FF'
def _dm_row_density(M):
"""Density measure for sparse matrices.
Defines the "density", ``d`` as the average number of non-zero entries per
row except ignoring rows that are fully zero. RREF can ignore fully zero
rows so they are excluded. By definition ``d >= 1`` except that we define
``d = 0`` for the zero matrix.
Returns ``(density, nrows_nz, ncols)`` where ``nrows_nz`` counts the number
of nonzero rows and ``ncols`` is the number of columns.
"""
# Uses the SDM dict-of-dicts representation.
ncols = M.shape[1]
rows_nz = M.rep.to_sdm().values()
if not rows_nz:
return 0, 0, ncols
else:
nrows_nz = len(rows_nz)
density = sum(map(len, rows_nz)) / nrows_nz
return density, nrows_nz, ncols
def _dm_elements(M):
"""Return nonzero elements of a DomainMatrix."""
elements, _ = M.to_flat_nz()
return elements
def _dm_QQ_numers_denoms(Mq):
"""Returns the numerators and denominators of a DomainMatrix over QQ."""
elements = _dm_elements(Mq)
numers = [e.numerator for e in elements]
denoms = [e.denominator for e in elements]
return numers, denoms
def _to_field(M):
"""Convert a DomainMatrix to a field if possible."""
K = M.domain
if K.has_assoc_Field:
return M.to_field()
else:
return M

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,558 @@
from sympy.testing.pytest import raises
from sympy.external.gmpy import GROUND_TYPES
from sympy.polys import ZZ, QQ
from sympy.polys.matrices.ddm import DDM
from sympy.polys.matrices.exceptions import (
DMShapeError, DMNonInvertibleMatrixError, DMDomainError,
DMBadInputError)
def test_DDM_init():
items = [[ZZ(0), ZZ(1), ZZ(2)], [ZZ(3), ZZ(4), ZZ(5)]]
shape = (2, 3)
ddm = DDM(items, shape, ZZ)
assert ddm.shape == shape
assert ddm.rows == 2
assert ddm.cols == 3
assert ddm.domain == ZZ
raises(DMBadInputError, lambda: DDM([[ZZ(2), ZZ(3)]], (2, 2), ZZ))
raises(DMBadInputError, lambda: DDM([[ZZ(1)], [ZZ(2), ZZ(3)]], (2, 2), ZZ))
def test_DDM_getsetitem():
ddm = DDM([[ZZ(2), ZZ(3)], [ZZ(4), ZZ(5)]], (2, 2), ZZ)
assert ddm[0][0] == ZZ(2)
assert ddm[0][1] == ZZ(3)
assert ddm[1][0] == ZZ(4)
assert ddm[1][1] == ZZ(5)
raises(IndexError, lambda: ddm[2][0])
raises(IndexError, lambda: ddm[0][2])
ddm[0][0] = ZZ(-1)
assert ddm[0][0] == ZZ(-1)
def test_DDM_str():
ddm = DDM([[ZZ(0), ZZ(1)], [ZZ(2), ZZ(3)]], (2, 2), ZZ)
if GROUND_TYPES == 'gmpy': # pragma: no cover
assert str(ddm) == '[[0, 1], [2, 3]]'
assert repr(ddm) == 'DDM([[mpz(0), mpz(1)], [mpz(2), mpz(3)]], (2, 2), ZZ)'
else: # pragma: no cover
assert repr(ddm) == 'DDM([[0, 1], [2, 3]], (2, 2), ZZ)'
assert str(ddm) == '[[0, 1], [2, 3]]'
def test_DDM_eq():
items = [[ZZ(0), ZZ(1)], [ZZ(2), ZZ(3)]]
ddm1 = DDM(items, (2, 2), ZZ)
ddm2 = DDM(items, (2, 2), ZZ)
assert (ddm1 == ddm1) is True
assert (ddm1 == items) is False
assert (items == ddm1) is False
assert (ddm1 == ddm2) is True
assert (ddm2 == ddm1) is True
assert (ddm1 != ddm1) is False
assert (ddm1 != items) is True
assert (items != ddm1) is True
assert (ddm1 != ddm2) is False
assert (ddm2 != ddm1) is False
ddm3 = DDM([[ZZ(0), ZZ(1)], [ZZ(3), ZZ(3)]], (2, 2), ZZ)
ddm3 = DDM(items, (2, 2), QQ)
assert (ddm1 == ddm3) is False
assert (ddm3 == ddm1) is False
assert (ddm1 != ddm3) is True
assert (ddm3 != ddm1) is True
def test_DDM_convert_to():
ddm = DDM([[ZZ(1), ZZ(2)]], (1, 2), ZZ)
assert ddm.convert_to(ZZ) == ddm
ddmq = ddm.convert_to(QQ)
assert ddmq.domain == QQ
def test_DDM_zeros():
ddmz = DDM.zeros((3, 4), QQ)
assert list(ddmz) == [[QQ(0)] * 4] * 3
assert ddmz.shape == (3, 4)
assert ddmz.domain == QQ
def test_DDM_ones():
ddmone = DDM.ones((2, 3), QQ)
assert list(ddmone) == [[QQ(1)] * 3] * 2
assert ddmone.shape == (2, 3)
assert ddmone.domain == QQ
def test_DDM_eye():
ddmz = DDM.eye(3, QQ)
f = lambda i, j: QQ(1) if i == j else QQ(0)
assert list(ddmz) == [[f(i, j) for i in range(3)] for j in range(3)]
assert ddmz.shape == (3, 3)
assert ddmz.domain == QQ
def test_DDM_copy():
ddm1 = DDM([[QQ(1)], [QQ(2)]], (2, 1), QQ)
ddm2 = ddm1.copy()
assert (ddm1 == ddm2) is True
ddm1[0][0] = QQ(-1)
assert (ddm1 == ddm2) is False
ddm2[0][0] = QQ(-1)
assert (ddm1 == ddm2) is True
def test_DDM_transpose():
ddm = DDM([[QQ(1)], [QQ(2)]], (2, 1), QQ)
ddmT = DDM([[QQ(1), QQ(2)]], (1, 2), QQ)
assert ddm.transpose() == ddmT
ddm02 = DDM([], (0, 2), QQ)
ddm02T = DDM([[], []], (2, 0), QQ)
assert ddm02.transpose() == ddm02T
assert ddm02T.transpose() == ddm02
ddm0 = DDM([], (0, 0), QQ)
assert ddm0.transpose() == ddm0
def test_DDM_add():
A = DDM([[ZZ(1)], [ZZ(2)]], (2, 1), ZZ)
B = DDM([[ZZ(3)], [ZZ(4)]], (2, 1), ZZ)
C = DDM([[ZZ(4)], [ZZ(6)]], (2, 1), ZZ)
AQ = DDM([[QQ(1)], [QQ(2)]], (2, 1), QQ)
assert A + B == A.add(B) == C
raises(DMShapeError, lambda: A + DDM([[ZZ(5)]], (1, 1), ZZ))
raises(TypeError, lambda: A + ZZ(1))
raises(TypeError, lambda: ZZ(1) + A)
raises(DMDomainError, lambda: A + AQ)
raises(DMDomainError, lambda: AQ + A)
def test_DDM_sub():
A = DDM([[ZZ(1)], [ZZ(2)]], (2, 1), ZZ)
B = DDM([[ZZ(3)], [ZZ(4)]], (2, 1), ZZ)
C = DDM([[ZZ(-2)], [ZZ(-2)]], (2, 1), ZZ)
AQ = DDM([[QQ(1)], [QQ(2)]], (2, 1), QQ)
D = DDM([[ZZ(5)]], (1, 1), ZZ)
assert A - B == A.sub(B) == C
raises(TypeError, lambda: A - ZZ(1))
raises(TypeError, lambda: ZZ(1) - A)
raises(DMShapeError, lambda: A - D)
raises(DMShapeError, lambda: D - A)
raises(DMShapeError, lambda: A.sub(D))
raises(DMShapeError, lambda: D.sub(A))
raises(DMDomainError, lambda: A - AQ)
raises(DMDomainError, lambda: AQ - A)
raises(DMDomainError, lambda: A.sub(AQ))
raises(DMDomainError, lambda: AQ.sub(A))
def test_DDM_neg():
A = DDM([[ZZ(1)], [ZZ(2)]], (2, 1), ZZ)
An = DDM([[ZZ(-1)], [ZZ(-2)]], (2, 1), ZZ)
assert -A == A.neg() == An
assert -An == An.neg() == A
def test_DDM_mul():
A = DDM([[ZZ(1)]], (1, 1), ZZ)
A2 = DDM([[ZZ(2)]], (1, 1), ZZ)
assert A * ZZ(2) == A2
assert ZZ(2) * A == A2
raises(TypeError, lambda: [[1]] * A)
raises(TypeError, lambda: A * [[1]])
def test_DDM_matmul():
A = DDM([[ZZ(1)], [ZZ(2)]], (2, 1), ZZ)
B = DDM([[ZZ(3), ZZ(4)]], (1, 2), ZZ)
AB = DDM([[ZZ(3), ZZ(4)], [ZZ(6), ZZ(8)]], (2, 2), ZZ)
BA = DDM([[ZZ(11)]], (1, 1), ZZ)
assert A @ B == A.matmul(B) == AB
assert B @ A == B.matmul(A) == BA
raises(TypeError, lambda: A @ 1)
raises(TypeError, lambda: A @ [[3, 4]])
Bq = DDM([[QQ(3), QQ(4)]], (1, 2), QQ)
raises(DMDomainError, lambda: A @ Bq)
raises(DMDomainError, lambda: Bq @ A)
C = DDM([[ZZ(1)]], (1, 1), ZZ)
assert A @ C == A.matmul(C) == A
raises(DMShapeError, lambda: C @ A)
raises(DMShapeError, lambda: C.matmul(A))
Z04 = DDM([], (0, 4), ZZ)
Z40 = DDM([[]]*4, (4, 0), ZZ)
Z50 = DDM([[]]*5, (5, 0), ZZ)
Z05 = DDM([], (0, 5), ZZ)
Z45 = DDM([[0] * 5] * 4, (4, 5), ZZ)
Z54 = DDM([[0] * 4] * 5, (5, 4), ZZ)
Z00 = DDM([], (0, 0), ZZ)
assert Z04 @ Z45 == Z04.matmul(Z45) == Z05
assert Z45 @ Z50 == Z45.matmul(Z50) == Z40
assert Z00 @ Z04 == Z00.matmul(Z04) == Z04
assert Z50 @ Z00 == Z50.matmul(Z00) == Z50
assert Z00 @ Z00 == Z00.matmul(Z00) == Z00
assert Z50 @ Z04 == Z50.matmul(Z04) == Z54
raises(DMShapeError, lambda: Z05 @ Z40)
raises(DMShapeError, lambda: Z05.matmul(Z40))
def test_DDM_hstack():
A = DDM([[ZZ(1), ZZ(2), ZZ(3)]], (1, 3), ZZ)
B = DDM([[ZZ(4), ZZ(5)]], (1, 2), ZZ)
C = DDM([[ZZ(6)]], (1, 1), ZZ)
Ah = A.hstack(B)
assert Ah.shape == (1, 5)
assert Ah.domain == ZZ
assert Ah == DDM([[ZZ(1), ZZ(2), ZZ(3), ZZ(4), ZZ(5)]], (1, 5), ZZ)
Ah = A.hstack(B, C)
assert Ah.shape == (1, 6)
assert Ah.domain == ZZ
assert Ah == DDM([[ZZ(1), ZZ(2), ZZ(3), ZZ(4), ZZ(5), ZZ(6)]], (1, 6), ZZ)
def test_DDM_vstack():
A = DDM([[ZZ(1)], [ZZ(2)], [ZZ(3)]], (3, 1), ZZ)
B = DDM([[ZZ(4)], [ZZ(5)]], (2, 1), ZZ)
C = DDM([[ZZ(6)]], (1, 1), ZZ)
Ah = A.vstack(B)
assert Ah.shape == (5, 1)
assert Ah.domain == ZZ
assert Ah == DDM([[ZZ(1)], [ZZ(2)], [ZZ(3)], [ZZ(4)], [ZZ(5)]], (5, 1), ZZ)
Ah = A.vstack(B, C)
assert Ah.shape == (6, 1)
assert Ah.domain == ZZ
assert Ah == DDM([[ZZ(1)], [ZZ(2)], [ZZ(3)], [ZZ(4)], [ZZ(5)], [ZZ(6)]], (6, 1), ZZ)
def test_DDM_applyfunc():
A = DDM([[ZZ(1), ZZ(2), ZZ(3)]], (1, 3), ZZ)
B = DDM([[ZZ(2), ZZ(4), ZZ(6)]], (1, 3), ZZ)
assert A.applyfunc(lambda x: 2*x, ZZ) == B
def test_DDM_rref():
A = DDM([], (0, 4), QQ)
assert A.rref() == (A, [])
A = DDM([[QQ(0), QQ(1)], [QQ(1), QQ(1)]], (2, 2), QQ)
Ar = DDM([[QQ(1), QQ(0)], [QQ(0), QQ(1)]], (2, 2), QQ)
pivots = [0, 1]
assert A.rref() == (Ar, pivots)
A = DDM([[QQ(1), QQ(2), QQ(1)], [QQ(3), QQ(4), QQ(1)]], (2, 3), QQ)
Ar = DDM([[QQ(1), QQ(0), QQ(-1)], [QQ(0), QQ(1), QQ(1)]], (2, 3), QQ)
pivots = [0, 1]
assert A.rref() == (Ar, pivots)
A = DDM([[QQ(3), QQ(4), QQ(1)], [QQ(1), QQ(2), QQ(1)]], (2, 3), QQ)
Ar = DDM([[QQ(1), QQ(0), QQ(-1)], [QQ(0), QQ(1), QQ(1)]], (2, 3), QQ)
pivots = [0, 1]
assert A.rref() == (Ar, pivots)
A = DDM([[QQ(1), QQ(0)], [QQ(1), QQ(3)], [QQ(0), QQ(1)]], (3, 2), QQ)
Ar = DDM([[QQ(1), QQ(0)], [QQ(0), QQ(1)], [QQ(0), QQ(0)]], (3, 2), QQ)
pivots = [0, 1]
assert A.rref() == (Ar, pivots)
A = DDM([[QQ(1), QQ(0), QQ(1)], [QQ(3), QQ(0), QQ(1)]], (2, 3), QQ)
Ar = DDM([[QQ(1), QQ(0), QQ(0)], [QQ(0), QQ(0), QQ(1)]], (2, 3), QQ)
pivots = [0, 2]
assert A.rref() == (Ar, pivots)
def test_DDM_nullspace():
# more tests are in test_nullspace.py
A = DDM([[QQ(1), QQ(1)], [QQ(1), QQ(1)]], (2, 2), QQ)
Anull = DDM([[QQ(-1), QQ(1)]], (1, 2), QQ)
nonpivots = [1]
assert A.nullspace() == (Anull, nonpivots)
def test_DDM_particular():
A = DDM([[QQ(1), QQ(0)]], (1, 2), QQ)
assert A.particular() == DDM.zeros((1, 1), QQ)
def test_DDM_det():
# 0x0 case
A = DDM([], (0, 0), ZZ)
assert A.det() == ZZ(1)
# 1x1 case
A = DDM([[ZZ(2)]], (1, 1), ZZ)
assert A.det() == ZZ(2)
# 2x2 case
A = DDM([[ZZ(1), ZZ(2)], [ZZ(3), ZZ(4)]], (2, 2), ZZ)
assert A.det() == ZZ(-2)
# 3x3 with swap
A = DDM([[ZZ(1), ZZ(2), ZZ(3)], [ZZ(1), ZZ(2), ZZ(4)], [ZZ(1), ZZ(2), ZZ(5)]], (3, 3), ZZ)
assert A.det() == ZZ(0)
# 2x2 QQ case
A = DDM([[QQ(1, 2), QQ(1, 2)], [QQ(1, 3), QQ(1, 4)]], (2, 2), QQ)
assert A.det() == QQ(-1, 24)
# Nonsquare error
A = DDM([[ZZ(1)], [ZZ(2)]], (2, 1), ZZ)
raises(DMShapeError, lambda: A.det())
# Nonsquare error with empty matrix
A = DDM([], (0, 1), ZZ)
raises(DMShapeError, lambda: A.det())
def test_DDM_inv():
A = DDM([[QQ(1, 1), QQ(2, 1)], [QQ(3, 1), QQ(4, 1)]], (2, 2), QQ)
Ainv = DDM([[QQ(-2, 1), QQ(1, 1)], [QQ(3, 2), QQ(-1, 2)]], (2, 2), QQ)
assert A.inv() == Ainv
A = DDM([[QQ(1), QQ(2)]], (1, 2), QQ)
raises(DMShapeError, lambda: A.inv())
A = DDM([[ZZ(2)]], (1, 1), ZZ)
raises(DMDomainError, lambda: A.inv())
A = DDM([], (0, 0), QQ)
assert A.inv() == A
A = DDM([[QQ(1), QQ(2)], [QQ(2), QQ(4)]], (2, 2), QQ)
raises(DMNonInvertibleMatrixError, lambda: A.inv())
def test_DDM_lu():
A = DDM([[QQ(1), QQ(2)], [QQ(3), QQ(4)]], (2, 2), QQ)
L, U, swaps = A.lu()
assert L == DDM([[QQ(1), QQ(0)], [QQ(3), QQ(1)]], (2, 2), QQ)
assert U == DDM([[QQ(1), QQ(2)], [QQ(0), QQ(-2)]], (2, 2), QQ)
assert swaps == []
A = [[1, 0, 0, 0], [0, 0, 0, 0], [0, 0, 1, 1], [0, 0, 1, 2]]
Lexp = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 1, 1]]
Uexp = [[1, 0, 0, 0], [0, 0, 0, 0], [0, 0, 1, 1], [0, 0, 0, 1]]
to_dom = lambda rows, dom: [[dom(e) for e in row] for row in rows]
A = DDM(to_dom(A, QQ), (4, 4), QQ)
Lexp = DDM(to_dom(Lexp, QQ), (4, 4), QQ)
Uexp = DDM(to_dom(Uexp, QQ), (4, 4), QQ)
L, U, swaps = A.lu()
assert L == Lexp
assert U == Uexp
assert swaps == []
def test_DDM_lu_solve():
# Basic example
A = DDM([[QQ(1), QQ(2)], [QQ(3), QQ(4)]], (2, 2), QQ)
b = DDM([[QQ(1)], [QQ(2)]], (2, 1), QQ)
x = DDM([[QQ(0)], [QQ(1, 2)]], (2, 1), QQ)
assert A.lu_solve(b) == x
# Example with swaps
A = DDM([[QQ(0), QQ(2)], [QQ(3), QQ(4)]], (2, 2), QQ)
assert A.lu_solve(b) == x
# Overdetermined, consistent
A = DDM([[QQ(1), QQ(2)], [QQ(3), QQ(4)], [QQ(5), QQ(6)]], (3, 2), QQ)
b = DDM([[QQ(1)], [QQ(2)], [QQ(3)]], (3, 1), QQ)
assert A.lu_solve(b) == x
# Overdetermined, inconsistent
b = DDM([[QQ(1)], [QQ(2)], [QQ(4)]], (3, 1), QQ)
raises(DMNonInvertibleMatrixError, lambda: A.lu_solve(b))
# Square, noninvertible
A = DDM([[QQ(1), QQ(2)], [QQ(1), QQ(2)]], (2, 2), QQ)
b = DDM([[QQ(1)], [QQ(2)]], (2, 1), QQ)
raises(DMNonInvertibleMatrixError, lambda: A.lu_solve(b))
# Underdetermined
A = DDM([[QQ(1), QQ(2)]], (1, 2), QQ)
b = DDM([[QQ(3)]], (1, 1), QQ)
raises(NotImplementedError, lambda: A.lu_solve(b))
# Domain mismatch
bz = DDM([[ZZ(1)], [ZZ(2)]], (2, 1), ZZ)
raises(DMDomainError, lambda: A.lu_solve(bz))
# Shape mismatch
b3 = DDM([[QQ(1)], [QQ(2)], [QQ(3)]], (3, 1), QQ)
raises(DMShapeError, lambda: A.lu_solve(b3))
def test_DDM_charpoly():
A = DDM([], (0, 0), ZZ)
assert A.charpoly() == [ZZ(1)]
A = DDM([
[ZZ(1), ZZ(2), ZZ(3)],
[ZZ(4), ZZ(5), ZZ(6)],
[ZZ(7), ZZ(8), ZZ(9)]], (3, 3), ZZ)
Avec = [ZZ(1), ZZ(-15), ZZ(-18), ZZ(0)]
assert A.charpoly() == Avec
A = DDM([[ZZ(1), ZZ(2)]], (1, 2), ZZ)
raises(DMShapeError, lambda: A.charpoly())
def test_DDM_getitem():
dm = DDM([
[ZZ(1), ZZ(2), ZZ(3)],
[ZZ(4), ZZ(5), ZZ(6)],
[ZZ(7), ZZ(8), ZZ(9)]], (3, 3), ZZ)
assert dm.getitem(1, 1) == ZZ(5)
assert dm.getitem(1, -2) == ZZ(5)
assert dm.getitem(-1, -3) == ZZ(7)
raises(IndexError, lambda: dm.getitem(3, 3))
def test_DDM_setitem():
dm = DDM.zeros((3, 3), ZZ)
dm.setitem(0, 0, 1)
dm.setitem(1, -2, 1)
dm.setitem(-1, -1, 1)
assert dm == DDM.eye(3, ZZ)
raises(IndexError, lambda: dm.setitem(3, 3, 0))
def test_DDM_extract_slice():
dm = DDM([
[ZZ(1), ZZ(2), ZZ(3)],
[ZZ(4), ZZ(5), ZZ(6)],
[ZZ(7), ZZ(8), ZZ(9)]], (3, 3), ZZ)
assert dm.extract_slice(slice(0, 3), slice(0, 3)) == dm
assert dm.extract_slice(slice(1, 3), slice(-2)) == DDM([[4], [7]], (2, 1), ZZ)
assert dm.extract_slice(slice(1, 3), slice(-2)) == DDM([[4], [7]], (2, 1), ZZ)
assert dm.extract_slice(slice(2, 3), slice(-2)) == DDM([[ZZ(7)]], (1, 1), ZZ)
assert dm.extract_slice(slice(0, 2), slice(-2)) == DDM([[1], [4]], (2, 1), ZZ)
assert dm.extract_slice(slice(-1), slice(-1)) == DDM([[1, 2], [4, 5]], (2, 2), ZZ)
assert dm.extract_slice(slice(2), slice(3, 4)) == DDM([[], []], (2, 0), ZZ)
assert dm.extract_slice(slice(3, 4), slice(2)) == DDM([], (0, 2), ZZ)
assert dm.extract_slice(slice(3, 4), slice(3, 4)) == DDM([], (0, 0), ZZ)
def test_DDM_extract():
dm1 = DDM([
[ZZ(1), ZZ(2), ZZ(3)],
[ZZ(4), ZZ(5), ZZ(6)],
[ZZ(7), ZZ(8), ZZ(9)]], (3, 3), ZZ)
dm2 = DDM([
[ZZ(6), ZZ(4)],
[ZZ(3), ZZ(1)]], (2, 2), ZZ)
assert dm1.extract([1, 0], [2, 0]) == dm2
assert dm1.extract([-2, 0], [-1, 0]) == dm2
assert dm1.extract([], []) == DDM.zeros((0, 0), ZZ)
assert dm1.extract([1], []) == DDM.zeros((1, 0), ZZ)
assert dm1.extract([], [1]) == DDM.zeros((0, 1), ZZ)
raises(IndexError, lambda: dm2.extract([2], [0]))
raises(IndexError, lambda: dm2.extract([0], [2]))
raises(IndexError, lambda: dm2.extract([-3], [0]))
raises(IndexError, lambda: dm2.extract([0], [-3]))
def test_DDM_flat():
dm = DDM([
[ZZ(6), ZZ(4)],
[ZZ(3), ZZ(1)]], (2, 2), ZZ)
assert dm.flat() == [ZZ(6), ZZ(4), ZZ(3), ZZ(1)]
def test_DDM_is_zero_matrix():
A = DDM([[QQ(1), QQ(0)], [QQ(0), QQ(0)]], (2, 2), QQ)
Azero = DDM.zeros((1, 2), QQ)
assert A.is_zero_matrix() is False
assert Azero.is_zero_matrix() is True
def test_DDM_is_upper():
# Wide matrices:
A = DDM([
[QQ(1), QQ(2), QQ(3), QQ(4)],
[QQ(0), QQ(5), QQ(6), QQ(7)],
[QQ(0), QQ(0), QQ(8), QQ(9)]
], (3, 4), QQ)
B = DDM([
[QQ(1), QQ(2), QQ(3), QQ(4)],
[QQ(0), QQ(5), QQ(6), QQ(7)],
[QQ(0), QQ(7), QQ(8), QQ(9)]
], (3, 4), QQ)
assert A.is_upper() is True
assert B.is_upper() is False
# Tall matrices:
A = DDM([
[QQ(1), QQ(2), QQ(3)],
[QQ(0), QQ(5), QQ(6)],
[QQ(0), QQ(0), QQ(8)],
[QQ(0), QQ(0), QQ(0)]
], (4, 3), QQ)
B = DDM([
[QQ(1), QQ(2), QQ(3)],
[QQ(0), QQ(5), QQ(6)],
[QQ(0), QQ(0), QQ(8)],
[QQ(0), QQ(0), QQ(10)]
], (4, 3), QQ)
assert A.is_upper() is True
assert B.is_upper() is False
def test_DDM_is_lower():
# Tall matrices:
A = DDM([
[QQ(1), QQ(2), QQ(3), QQ(4)],
[QQ(0), QQ(5), QQ(6), QQ(7)],
[QQ(0), QQ(0), QQ(8), QQ(9)]
], (3, 4), QQ).transpose()
B = DDM([
[QQ(1), QQ(2), QQ(3), QQ(4)],
[QQ(0), QQ(5), QQ(6), QQ(7)],
[QQ(0), QQ(7), QQ(8), QQ(9)]
], (3, 4), QQ).transpose()
assert A.is_lower() is True
assert B.is_lower() is False
# Wide matrices:
A = DDM([
[QQ(1), QQ(2), QQ(3)],
[QQ(0), QQ(5), QQ(6)],
[QQ(0), QQ(0), QQ(8)],
[QQ(0), QQ(0), QQ(0)]
], (4, 3), QQ).transpose()
B = DDM([
[QQ(1), QQ(2), QQ(3)],
[QQ(0), QQ(5), QQ(6)],
[QQ(0), QQ(0), QQ(8)],
[QQ(0), QQ(0), QQ(10)]
], (4, 3), QQ).transpose()
assert A.is_lower() is True
assert B.is_lower() is False

View File

@ -0,0 +1,350 @@
from sympy.testing.pytest import raises
from sympy.polys import ZZ, QQ
from sympy.polys.matrices.ddm import DDM
from sympy.polys.matrices.dense import (
ddm_transpose,
ddm_iadd, ddm_isub, ddm_ineg, ddm_imatmul, ddm_imul, ddm_irref,
ddm_idet, ddm_iinv, ddm_ilu, ddm_ilu_split, ddm_ilu_solve, ddm_berk)
from sympy.polys.matrices.exceptions import (
DMDomainError,
DMNonInvertibleMatrixError,
DMNonSquareMatrixError,
DMShapeError,
)
def test_ddm_transpose():
a = [[1, 2], [3, 4]]
assert ddm_transpose(a) == [[1, 3], [2, 4]]
def test_ddm_iadd():
a = [[1, 2], [3, 4]]
b = [[5, 6], [7, 8]]
ddm_iadd(a, b)
assert a == [[6, 8], [10, 12]]
def test_ddm_isub():
a = [[1, 2], [3, 4]]
b = [[5, 6], [7, 8]]
ddm_isub(a, b)
assert a == [[-4, -4], [-4, -4]]
def test_ddm_ineg():
a = [[1, 2], [3, 4]]
ddm_ineg(a)
assert a == [[-1, -2], [-3, -4]]
def test_ddm_matmul():
a = [[1, 2], [3, 4]]
ddm_imul(a, 2)
assert a == [[2, 4], [6, 8]]
a = [[1, 2], [3, 4]]
ddm_imul(a, 0)
assert a == [[0, 0], [0, 0]]
def test_ddm_imatmul():
a = [[1, 2, 3], [4, 5, 6]]
b = [[1, 2], [3, 4], [5, 6]]
c1 = [[0, 0], [0, 0]]
ddm_imatmul(c1, a, b)
assert c1 == [[22, 28], [49, 64]]
c2 = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
ddm_imatmul(c2, b, a)
assert c2 == [[9, 12, 15], [19, 26, 33], [29, 40, 51]]
b3 = [[1], [2], [3]]
c3 = [[0], [0]]
ddm_imatmul(c3, a, b3)
assert c3 == [[14], [32]]
def test_ddm_irref():
# Empty matrix
A = []
Ar = []
pivots = []
assert ddm_irref(A) == pivots
assert A == Ar
# Standard square case
A = [[QQ(0), QQ(1)], [QQ(1), QQ(1)]]
Ar = [[QQ(1), QQ(0)], [QQ(0), QQ(1)]]
pivots = [0, 1]
assert ddm_irref(A) == pivots
assert A == Ar
# m < n case
A = [[QQ(1), QQ(2), QQ(1)], [QQ(3), QQ(4), QQ(1)]]
Ar = [[QQ(1), QQ(0), QQ(-1)], [QQ(0), QQ(1), QQ(1)]]
pivots = [0, 1]
assert ddm_irref(A) == pivots
assert A == Ar
# same m < n but reversed
A = [[QQ(3), QQ(4), QQ(1)], [QQ(1), QQ(2), QQ(1)]]
Ar = [[QQ(1), QQ(0), QQ(-1)], [QQ(0), QQ(1), QQ(1)]]
pivots = [0, 1]
assert ddm_irref(A) == pivots
assert A == Ar
# m > n case
A = [[QQ(1), QQ(0)], [QQ(1), QQ(3)], [QQ(0), QQ(1)]]
Ar = [[QQ(1), QQ(0)], [QQ(0), QQ(1)], [QQ(0), QQ(0)]]
pivots = [0, 1]
assert ddm_irref(A) == pivots
assert A == Ar
# Example with missing pivot
A = [[QQ(1), QQ(0), QQ(1)], [QQ(3), QQ(0), QQ(1)]]
Ar = [[QQ(1), QQ(0), QQ(0)], [QQ(0), QQ(0), QQ(1)]]
pivots = [0, 2]
assert ddm_irref(A) == pivots
assert A == Ar
# Example with missing pivot and no replacement
A = [[QQ(0), QQ(1)], [QQ(0), QQ(2)], [QQ(1), QQ(0)]]
Ar = [[QQ(1), QQ(0)], [QQ(0), QQ(1)], [QQ(0), QQ(0)]]
pivots = [0, 1]
assert ddm_irref(A) == pivots
assert A == Ar
def test_ddm_idet():
A = []
assert ddm_idet(A, ZZ) == ZZ(1)
A = [[ZZ(2)]]
assert ddm_idet(A, ZZ) == ZZ(2)
A = [[ZZ(1), ZZ(2)], [ZZ(3), ZZ(4)]]
assert ddm_idet(A, ZZ) == ZZ(-2)
A = [[ZZ(1), ZZ(2), ZZ(3)], [ZZ(1), ZZ(2), ZZ(4)], [ZZ(1), ZZ(3), ZZ(5)]]
assert ddm_idet(A, ZZ) == ZZ(-1)
A = [[ZZ(1), ZZ(2), ZZ(3)], [ZZ(1), ZZ(2), ZZ(4)], [ZZ(1), ZZ(2), ZZ(5)]]
assert ddm_idet(A, ZZ) == ZZ(0)
A = [[QQ(1, 2), QQ(1, 2)], [QQ(1, 3), QQ(1, 4)]]
assert ddm_idet(A, QQ) == QQ(-1, 24)
def test_ddm_inv():
A = []
Ainv = []
ddm_iinv(Ainv, A, QQ)
assert Ainv == A
A = []
Ainv = []
raises(DMDomainError, lambda: ddm_iinv(Ainv, A, ZZ))
A = [[QQ(1), QQ(2)]]
Ainv = [[QQ(0), QQ(0)]]
raises(DMNonSquareMatrixError, lambda: ddm_iinv(Ainv, A, QQ))
A = [[QQ(1, 1), QQ(2, 1)], [QQ(3, 1), QQ(4, 1)]]
Ainv = [[QQ(0), QQ(0)], [QQ(0), QQ(0)]]
Ainv_expected = [[QQ(-2, 1), QQ(1, 1)], [QQ(3, 2), QQ(-1, 2)]]
ddm_iinv(Ainv, A, QQ)
assert Ainv == Ainv_expected
A = [[QQ(1, 1), QQ(2, 1)], [QQ(2, 1), QQ(4, 1)]]
Ainv = [[QQ(0), QQ(0)], [QQ(0), QQ(0)]]
raises(DMNonInvertibleMatrixError, lambda: ddm_iinv(Ainv, A, QQ))
def test_ddm_ilu():
A = []
Alu = []
swaps = ddm_ilu(A)
assert A == Alu
assert swaps == []
A = [[]]
Alu = [[]]
swaps = ddm_ilu(A)
assert A == Alu
assert swaps == []
A = [[QQ(1), QQ(2)], [QQ(3), QQ(4)]]
Alu = [[QQ(1), QQ(2)], [QQ(3), QQ(-2)]]
swaps = ddm_ilu(A)
assert A == Alu
assert swaps == []
A = [[QQ(0), QQ(2)], [QQ(3), QQ(4)]]
Alu = [[QQ(3), QQ(4)], [QQ(0), QQ(2)]]
swaps = ddm_ilu(A)
assert A == Alu
assert swaps == [(0, 1)]
A = [[QQ(1), QQ(2), QQ(3)], [QQ(4), QQ(5), QQ(6)], [QQ(7), QQ(8), QQ(9)]]
Alu = [[QQ(1), QQ(2), QQ(3)], [QQ(4), QQ(-3), QQ(-6)], [QQ(7), QQ(2), QQ(0)]]
swaps = ddm_ilu(A)
assert A == Alu
assert swaps == []
A = [[QQ(0), QQ(1), QQ(2)], [QQ(0), QQ(1), QQ(3)], [QQ(1), QQ(1), QQ(2)]]
Alu = [[QQ(1), QQ(1), QQ(2)], [QQ(0), QQ(1), QQ(3)], [QQ(0), QQ(1), QQ(-1)]]
swaps = ddm_ilu(A)
assert A == Alu
assert swaps == [(0, 2)]
A = [[QQ(1), QQ(2), QQ(3)], [QQ(4), QQ(5), QQ(6)]]
Alu = [[QQ(1), QQ(2), QQ(3)], [QQ(4), QQ(-3), QQ(-6)]]
swaps = ddm_ilu(A)
assert A == Alu
assert swaps == []
A = [[QQ(1), QQ(2)], [QQ(3), QQ(4)], [QQ(5), QQ(6)]]
Alu = [[QQ(1), QQ(2)], [QQ(3), QQ(-2)], [QQ(5), QQ(2)]]
swaps = ddm_ilu(A)
assert A == Alu
assert swaps == []
def test_ddm_ilu_split():
U = []
L = []
Uexp = []
Lexp = []
swaps = ddm_ilu_split(L, U, QQ)
assert U == Uexp
assert L == Lexp
assert swaps == []
U = [[]]
L = [[QQ(1)]]
Uexp = [[]]
Lexp = [[QQ(1)]]
swaps = ddm_ilu_split(L, U, QQ)
assert U == Uexp
assert L == Lexp
assert swaps == []
U = [[QQ(1), QQ(2)], [QQ(3), QQ(4)]]
L = [[QQ(1), QQ(0)], [QQ(0), QQ(1)]]
Uexp = [[QQ(1), QQ(2)], [QQ(0), QQ(-2)]]
Lexp = [[QQ(1), QQ(0)], [QQ(3), QQ(1)]]
swaps = ddm_ilu_split(L, U, QQ)
assert U == Uexp
assert L == Lexp
assert swaps == []
U = [[QQ(1), QQ(2), QQ(3)], [QQ(4), QQ(5), QQ(6)]]
L = [[QQ(1), QQ(0)], [QQ(0), QQ(1)]]
Uexp = [[QQ(1), QQ(2), QQ(3)], [QQ(0), QQ(-3), QQ(-6)]]
Lexp = [[QQ(1), QQ(0)], [QQ(4), QQ(1)]]
swaps = ddm_ilu_split(L, U, QQ)
assert U == Uexp
assert L == Lexp
assert swaps == []
U = [[QQ(1), QQ(2)], [QQ(3), QQ(4)], [QQ(5), QQ(6)]]
L = [[QQ(1), QQ(0), QQ(0)], [QQ(0), QQ(1), QQ(0)], [QQ(0), QQ(0), QQ(1)]]
Uexp = [[QQ(1), QQ(2)], [QQ(0), QQ(-2)], [QQ(0), QQ(0)]]
Lexp = [[QQ(1), QQ(0), QQ(0)], [QQ(3), QQ(1), QQ(0)], [QQ(5), QQ(2), QQ(1)]]
swaps = ddm_ilu_split(L, U, QQ)
assert U == Uexp
assert L == Lexp
assert swaps == []
def test_ddm_ilu_solve():
# Basic example
# A = [[QQ(1), QQ(2)], [QQ(3), QQ(4)]]
U = [[QQ(1), QQ(2)], [QQ(0), QQ(-2)]]
L = [[QQ(1), QQ(0)], [QQ(3), QQ(1)]]
swaps = []
b = DDM([[QQ(1)], [QQ(2)]], (2, 1), QQ)
x = DDM([[QQ(0)], [QQ(0)]], (2, 1), QQ)
xexp = DDM([[QQ(0)], [QQ(1, 2)]], (2, 1), QQ)
ddm_ilu_solve(x, L, U, swaps, b)
assert x == xexp
# Example with swaps
# A = [[QQ(0), QQ(2)], [QQ(3), QQ(4)]]
U = [[QQ(3), QQ(4)], [QQ(0), QQ(2)]]
L = [[QQ(1), QQ(0)], [QQ(0), QQ(1)]]
swaps = [(0, 1)]
b = DDM([[QQ(1)], [QQ(2)]], (2, 1), QQ)
x = DDM([[QQ(0)], [QQ(0)]], (2, 1), QQ)
xexp = DDM([[QQ(0)], [QQ(1, 2)]], (2, 1), QQ)
ddm_ilu_solve(x, L, U, swaps, b)
assert x == xexp
# Overdetermined, consistent
# A = DDM([[QQ(1), QQ(2)], [QQ(3), QQ(4)], [QQ(5), QQ(6)]], (3, 2), QQ)
U = [[QQ(1), QQ(2)], [QQ(0), QQ(-2)], [QQ(0), QQ(0)]]
L = [[QQ(1), QQ(0), QQ(0)], [QQ(3), QQ(1), QQ(0)], [QQ(5), QQ(2), QQ(1)]]
swaps = []
b = DDM([[QQ(1)], [QQ(2)], [QQ(3)]], (3, 1), QQ)
x = DDM([[QQ(0)], [QQ(0)]], (2, 1), QQ)
xexp = DDM([[QQ(0)], [QQ(1, 2)]], (2, 1), QQ)
ddm_ilu_solve(x, L, U, swaps, b)
assert x == xexp
# Overdetermined, inconsistent
b = DDM([[QQ(1)], [QQ(2)], [QQ(4)]], (3, 1), QQ)
raises(DMNonInvertibleMatrixError, lambda: ddm_ilu_solve(x, L, U, swaps, b))
# Square, noninvertible
# A = DDM([[QQ(1), QQ(2)], [QQ(1), QQ(2)]], (2, 2), QQ)
U = [[QQ(1), QQ(2)], [QQ(0), QQ(0)]]
L = [[QQ(1), QQ(0)], [QQ(1), QQ(1)]]
swaps = []
b = DDM([[QQ(1)], [QQ(2)]], (2, 1), QQ)
raises(DMNonInvertibleMatrixError, lambda: ddm_ilu_solve(x, L, U, swaps, b))
# Underdetermined
# A = DDM([[QQ(1), QQ(2)]], (1, 2), QQ)
U = [[QQ(1), QQ(2)]]
L = [[QQ(1)]]
swaps = []
b = DDM([[QQ(3)]], (1, 1), QQ)
raises(NotImplementedError, lambda: ddm_ilu_solve(x, L, U, swaps, b))
# Shape mismatch
b3 = DDM([[QQ(1)], [QQ(2)], [QQ(3)]], (3, 1), QQ)
raises(DMShapeError, lambda: ddm_ilu_solve(x, L, U, swaps, b3))
# Empty shape mismatch
U = [[QQ(1)]]
L = [[QQ(1)]]
swaps = []
x = [[QQ(1)]]
b = []
raises(DMShapeError, lambda: ddm_ilu_solve(x, L, U, swaps, b))
# Empty system
U = []
L = []
swaps = []
b = []
x = []
ddm_ilu_solve(x, L, U, swaps, b)
assert x == []
def test_ddm_charpoly():
A = []
assert ddm_berk(A, ZZ) == [[ZZ(1)]]
A = [[ZZ(1), ZZ(2), ZZ(3)], [ZZ(4), ZZ(5), ZZ(6)], [ZZ(7), ZZ(8), ZZ(9)]]
Avec = [[ZZ(1)], [ZZ(-15)], [ZZ(-18)], [ZZ(0)]]
assert ddm_berk(A, ZZ) == Avec
A = DDM([[ZZ(1), ZZ(2)]], (1, 2), ZZ)
raises(DMShapeError, lambda: ddm_berk(A, ZZ))

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,153 @@
from sympy.testing.pytest import raises
from sympy.core.symbol import S
from sympy.polys import ZZ, QQ
from sympy.polys.matrices.domainscalar import DomainScalar
from sympy.polys.matrices.domainmatrix import DomainMatrix
def test_DomainScalar___new__():
raises(TypeError, lambda: DomainScalar(ZZ(1), QQ))
raises(TypeError, lambda: DomainScalar(ZZ(1), 1))
def test_DomainScalar_new():
A = DomainScalar(ZZ(1), ZZ)
B = A.new(ZZ(4), ZZ)
assert B == DomainScalar(ZZ(4), ZZ)
def test_DomainScalar_repr():
A = DomainScalar(ZZ(1), ZZ)
assert repr(A) in {'1', 'mpz(1)'}
def test_DomainScalar_from_sympy():
expr = S(1)
B = DomainScalar.from_sympy(expr)
assert B == DomainScalar(ZZ(1), ZZ)
def test_DomainScalar_to_sympy():
B = DomainScalar(ZZ(1), ZZ)
expr = B.to_sympy()
assert expr.is_Integer and expr == 1
def test_DomainScalar_to_domain():
A = DomainScalar(ZZ(1), ZZ)
B = A.to_domain(QQ)
assert B == DomainScalar(QQ(1), QQ)
def test_DomainScalar_convert_to():
A = DomainScalar(ZZ(1), ZZ)
B = A.convert_to(QQ)
assert B == DomainScalar(QQ(1), QQ)
def test_DomainScalar_unify():
A = DomainScalar(ZZ(1), ZZ)
B = DomainScalar(QQ(2), QQ)
A, B = A.unify(B)
assert A.domain == B.domain == QQ
def test_DomainScalar_add():
A = DomainScalar(ZZ(1), ZZ)
B = DomainScalar(QQ(2), QQ)
assert A + B == DomainScalar(QQ(3), QQ)
raises(TypeError, lambda: A + 1.5)
def test_DomainScalar_sub():
A = DomainScalar(ZZ(1), ZZ)
B = DomainScalar(QQ(2), QQ)
assert A - B == DomainScalar(QQ(-1), QQ)
raises(TypeError, lambda: A - 1.5)
def test_DomainScalar_mul():
A = DomainScalar(ZZ(1), ZZ)
B = DomainScalar(QQ(2), QQ)
dm = DomainMatrix([[ZZ(1), ZZ(2)], [ZZ(3), ZZ(4)]], (2, 2), ZZ)
assert A * B == DomainScalar(QQ(2), QQ)
assert A * dm == dm
assert B * 2 == DomainScalar(QQ(4), QQ)
raises(TypeError, lambda: A * 1.5)
def test_DomainScalar_floordiv():
A = DomainScalar(ZZ(-5), ZZ)
B = DomainScalar(QQ(2), QQ)
assert A // B == DomainScalar(QQ(-5, 2), QQ)
C = DomainScalar(ZZ(2), ZZ)
assert A // C == DomainScalar(ZZ(-3), ZZ)
raises(TypeError, lambda: A // 1.5)
def test_DomainScalar_mod():
A = DomainScalar(ZZ(5), ZZ)
B = DomainScalar(QQ(2), QQ)
assert A % B == DomainScalar(QQ(0), QQ)
C = DomainScalar(ZZ(2), ZZ)
assert A % C == DomainScalar(ZZ(1), ZZ)
raises(TypeError, lambda: A % 1.5)
def test_DomainScalar_divmod():
A = DomainScalar(ZZ(5), ZZ)
B = DomainScalar(QQ(2), QQ)
assert divmod(A, B) == (DomainScalar(QQ(5, 2), QQ), DomainScalar(QQ(0), QQ))
C = DomainScalar(ZZ(2), ZZ)
assert divmod(A, C) == (DomainScalar(ZZ(2), ZZ), DomainScalar(ZZ(1), ZZ))
raises(TypeError, lambda: divmod(A, 1.5))
def test_DomainScalar_pow():
A = DomainScalar(ZZ(-5), ZZ)
B = A**(2)
assert B == DomainScalar(ZZ(25), ZZ)
raises(TypeError, lambda: A**(1.5))
def test_DomainScalar_pos():
A = DomainScalar(QQ(2), QQ)
B = DomainScalar(QQ(2), QQ)
assert +A == B
def test_DomainScalar_neg():
A = DomainScalar(QQ(2), QQ)
B = DomainScalar(QQ(-2), QQ)
assert -A == B
def test_DomainScalar_eq():
A = DomainScalar(QQ(2), QQ)
assert A == A
B = DomainScalar(ZZ(-5), ZZ)
assert A != B
C = DomainScalar(ZZ(2), ZZ)
assert A != C
D = [1]
assert A != D
def test_DomainScalar_isZero():
A = DomainScalar(ZZ(0), ZZ)
assert A.is_zero() == True
B = DomainScalar(ZZ(1), ZZ)
assert B.is_zero() == False
def test_DomainScalar_isOne():
A = DomainScalar(ZZ(1), ZZ)
assert A.is_one() == True
B = DomainScalar(ZZ(0), ZZ)
assert B.is_one() == False

View File

@ -0,0 +1,90 @@
"""
Tests for the sympy.polys.matrices.eigen module
"""
from sympy.core.singleton import S
from sympy.functions.elementary.miscellaneous import sqrt
from sympy.matrices.dense import Matrix
from sympy.polys.agca.extensions import FiniteExtension
from sympy.polys.domains import QQ
from sympy.polys.polytools import Poly
from sympy.polys.rootoftools import CRootOf
from sympy.polys.matrices.domainmatrix import DomainMatrix
from sympy.polys.matrices.eigen import dom_eigenvects, dom_eigenvects_to_sympy
def test_dom_eigenvects_rational():
# Rational eigenvalues
A = DomainMatrix([[QQ(1), QQ(2)], [QQ(1), QQ(2)]], (2, 2), QQ)
rational_eigenvects = [
(QQ, QQ(3), 1, DomainMatrix([[QQ(1), QQ(1)]], (1, 2), QQ)),
(QQ, QQ(0), 1, DomainMatrix([[QQ(-2), QQ(1)]], (1, 2), QQ)),
]
assert dom_eigenvects(A) == (rational_eigenvects, [])
# Test converting to Expr:
sympy_eigenvects = [
(S(3), 1, [Matrix([1, 1])]),
(S(0), 1, [Matrix([-2, 1])]),
]
assert dom_eigenvects_to_sympy(rational_eigenvects, [], Matrix) == sympy_eigenvects
def test_dom_eigenvects_algebraic():
# Algebraic eigenvalues
A = DomainMatrix([[QQ(1), QQ(2)], [QQ(3), QQ(4)]], (2, 2), QQ)
Avects = dom_eigenvects(A)
# Extract the dummy to build the expected result:
lamda = Avects[1][0][1].gens[0]
irreducible = Poly(lamda**2 - 5*lamda - 2, lamda, domain=QQ)
K = FiniteExtension(irreducible)
KK = K.from_sympy
algebraic_eigenvects = [
(K, irreducible, 1, DomainMatrix([[KK((lamda-4)/3), KK(1)]], (1, 2), K)),
]
assert Avects == ([], algebraic_eigenvects)
# Test converting to Expr:
sympy_eigenvects = [
(S(5)/2 - sqrt(33)/2, 1, [Matrix([[-sqrt(33)/6 - S(1)/2], [1]])]),
(S(5)/2 + sqrt(33)/2, 1, [Matrix([[-S(1)/2 + sqrt(33)/6], [1]])]),
]
assert dom_eigenvects_to_sympy([], algebraic_eigenvects, Matrix) == sympy_eigenvects
def test_dom_eigenvects_rootof():
# Algebraic eigenvalues
A = DomainMatrix([
[0, 0, 0, 0, -1],
[1, 0, 0, 0, 1],
[0, 1, 0, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 0, 1, 0]], (5, 5), QQ)
Avects = dom_eigenvects(A)
# Extract the dummy to build the expected result:
lamda = Avects[1][0][1].gens[0]
irreducible = Poly(lamda**5 - lamda + 1, lamda, domain=QQ)
K = FiniteExtension(irreducible)
KK = K.from_sympy
algebraic_eigenvects = [
(K, irreducible, 1,
DomainMatrix([
[KK(lamda**4-1), KK(lamda**3), KK(lamda**2), KK(lamda), KK(1)]
], (1, 5), K)),
]
assert Avects == ([], algebraic_eigenvects)
# Test converting to Expr (slow):
l0, l1, l2, l3, l4 = [CRootOf(lamda**5 - lamda + 1, i) for i in range(5)]
sympy_eigenvects = [
(l0, 1, [Matrix([-1 + l0**4, l0**3, l0**2, l0, 1])]),
(l1, 1, [Matrix([-1 + l1**4, l1**3, l1**2, l1, 1])]),
(l2, 1, [Matrix([-1 + l2**4, l2**3, l2**2, l2, 1])]),
(l3, 1, [Matrix([-1 + l3**4, l3**3, l3**2, l3, 1])]),
(l4, 1, [Matrix([-1 + l4**4, l4**3, l4**2, l4, 1])]),
]
assert dom_eigenvects_to_sympy([], algebraic_eigenvects, Matrix) == sympy_eigenvects

View File

@ -0,0 +1,193 @@
from sympy import ZZ, Matrix
from sympy.polys.matrices import DM, DomainMatrix
from sympy.polys.matrices.dense import ddm_iinv
from sympy.polys.matrices.exceptions import DMNonInvertibleMatrixError
from sympy.matrices.exceptions import NonInvertibleMatrixError
import pytest
from sympy.testing.pytest import raises
from sympy.core.numbers import all_close
from sympy.abc import x
# Examples are given as adjugate matrix and determinant adj_det should match
# these exactly but inv_den only matches after cancel_denom.
INVERSE_EXAMPLES = [
(
'zz_1',
DomainMatrix([], (0, 0), ZZ),
DomainMatrix([], (0, 0), ZZ),
ZZ(1),
),
(
'zz_2',
DM([[2]], ZZ),
DM([[1]], ZZ),
ZZ(2),
),
(
'zz_3',
DM([[2, 0],
[0, 2]], ZZ),
DM([[2, 0],
[0, 2]], ZZ),
ZZ(4),
),
(
'zz_4',
DM([[1, 2],
[3, 4]], ZZ),
DM([[ 4, -2],
[-3, 1]], ZZ),
ZZ(-2),
),
(
'zz_5',
DM([[2, 2, 0],
[0, 2, 2],
[0, 0, 2]], ZZ),
DM([[4, -4, 4],
[0, 4, -4],
[0, 0, 4]], ZZ),
ZZ(8),
),
(
'zz_6',
DM([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]], ZZ),
DM([[-3, 6, -3],
[ 6, -12, 6],
[-3, 6, -3]], ZZ),
ZZ(0),
),
]
@pytest.mark.parametrize('name, A, A_inv, den', INVERSE_EXAMPLES)
def test_Matrix_inv(name, A, A_inv, den):
def _check(**kwargs):
if den != 0:
assert A.inv(**kwargs) == A_inv
else:
raises(NonInvertibleMatrixError, lambda: A.inv(**kwargs))
K = A.domain
A = A.to_Matrix()
A_inv = A_inv.to_Matrix() / K.to_sympy(den)
_check()
for method in ['GE', 'LU', 'ADJ', 'CH', 'LDL', 'QR']:
_check(method=method)
@pytest.mark.parametrize('name, A, A_inv, den', INVERSE_EXAMPLES)
def test_dm_inv_den(name, A, A_inv, den):
if den != 0:
A_inv_f, den_f = A.inv_den()
assert A_inv_f.cancel_denom(den_f) == A_inv.cancel_denom(den)
else:
raises(DMNonInvertibleMatrixError, lambda: A.inv_den())
@pytest.mark.parametrize('name, A, A_inv, den', INVERSE_EXAMPLES)
def test_dm_inv(name, A, A_inv, den):
A = A.to_field()
if den != 0:
A_inv = A_inv.to_field() / den
assert A.inv() == A_inv
else:
raises(DMNonInvertibleMatrixError, lambda: A.inv())
@pytest.mark.parametrize('name, A, A_inv, den', INVERSE_EXAMPLES)
def test_ddm_inv(name, A, A_inv, den):
A = A.to_field().to_ddm()
if den != 0:
A_inv = (A_inv.to_field() / den).to_ddm()
assert A.inv() == A_inv
else:
raises(DMNonInvertibleMatrixError, lambda: A.inv())
@pytest.mark.parametrize('name, A, A_inv, den', INVERSE_EXAMPLES)
def test_sdm_inv(name, A, A_inv, den):
A = A.to_field().to_sdm()
if den != 0:
A_inv = (A_inv.to_field() / den).to_sdm()
assert A.inv() == A_inv
else:
raises(DMNonInvertibleMatrixError, lambda: A.inv())
@pytest.mark.parametrize('name, A, A_inv, den', INVERSE_EXAMPLES)
def test_dense_ddm_iinv(name, A, A_inv, den):
A = A.to_field().to_ddm().copy()
K = A.domain
A_result = A.copy()
if den != 0:
A_inv = (A_inv.to_field() / den).to_ddm()
ddm_iinv(A_result, A, K)
assert A_result == A_inv
else:
raises(DMNonInvertibleMatrixError, lambda: ddm_iinv(A_result, A, K))
@pytest.mark.parametrize('name, A, A_inv, den', INVERSE_EXAMPLES)
def test_Matrix_adjugate(name, A, A_inv, den):
A = A.to_Matrix()
A_inv = A_inv.to_Matrix()
assert A.adjugate() == A_inv
for method in ["bareiss", "berkowitz", "bird", "laplace", "lu"]:
assert A.adjugate(method=method) == A_inv
@pytest.mark.parametrize('name, A, A_inv, den', INVERSE_EXAMPLES)
def test_dm_adj_det(name, A, A_inv, den):
assert A.adj_det() == (A_inv, den)
def test_inverse_inexact():
M = Matrix([[x-0.3, -0.06, -0.22],
[-0.46, x-0.48, -0.41],
[-0.14, -0.39, x-0.64]])
Mn = Matrix([[1.0*x**2 - 1.12*x + 0.1473, 0.06*x + 0.0474, 0.22*x - 0.081],
[0.46*x - 0.237, 1.0*x**2 - 0.94*x + 0.1612, 0.41*x - 0.0218],
[0.14*x + 0.1122, 0.39*x - 0.1086, 1.0*x**2 - 0.78*x + 0.1164]])
d = 1.0*x**3 - 1.42*x**2 + 0.4249*x - 0.0546540000000002
Mi = Mn / d
M_dm = M.to_DM()
M_dmd = M_dm.to_dense()
M_dm_num, M_dm_den = M_dm.inv_den()
M_dmd_num, M_dmd_den = M_dmd.inv_den()
# XXX: We don't check M_dm().to_field().inv() which currently uses division
# and produces a more complicate result from gcd cancellation failing.
# DomainMatrix.inv() over RR(x) should be changed to clear denominators and
# use DomainMatrix.inv_den().
Minvs = [
M.inv(),
(M_dm_num.to_field() / M_dm_den).to_Matrix(),
(M_dmd_num.to_field() / M_dmd_den).to_Matrix(),
M_dm_num.to_Matrix() / M_dm_den.as_expr(),
M_dmd_num.to_Matrix() / M_dmd_den.as_expr(),
]
for Minv in Minvs:
for Mi1, Mi2 in zip(Minv.flat(), Mi.flat()):
assert all_close(Mi2, Mi1)

View File

@ -0,0 +1,111 @@
#
# test_linsolve.py
#
# Test the internal implementation of linsolve.
#
from sympy.testing.pytest import raises
from sympy.core.numbers import I
from sympy.core.relational import Eq
from sympy.core.singleton import S
from sympy.abc import x, y, z
from sympy.polys.matrices.linsolve import _linsolve
from sympy.polys.solvers import PolyNonlinearError
def test__linsolve():
assert _linsolve([], [x]) == {x:x}
assert _linsolve([S.Zero], [x]) == {x:x}
assert _linsolve([x-1,x-2], [x]) is None
assert _linsolve([x-1], [x]) == {x:1}
assert _linsolve([x-1, y], [x, y]) == {x:1, y:S.Zero}
assert _linsolve([2*I], [x]) is None
raises(PolyNonlinearError, lambda: _linsolve([x*(1 + x)], [x]))
def test__linsolve_float():
# This should give the exact answer:
eqs = [
y - x,
y - 0.0216 * x
]
sol = {x:0.0, y:0.0}
assert _linsolve(eqs, (x, y)) == sol
# Other cases should be close to eps
def all_close(sol1, sol2, eps=1e-15):
close = lambda a, b: abs(a - b) < eps
assert sol1.keys() == sol2.keys()
return all(close(sol1[s], sol2[s]) for s in sol1)
eqs = [
0.8*x + 0.8*z + 0.2,
0.9*x + 0.7*y + 0.2*z + 0.9,
0.7*x + 0.2*y + 0.2*z + 0.5
]
sol_exact = {x:-29/42, y:-11/21, z:37/84}
sol_linsolve = _linsolve(eqs, [x,y,z])
assert all_close(sol_exact, sol_linsolve)
eqs = [
0.9*x + 0.3*y + 0.4*z + 0.6,
0.6*x + 0.9*y + 0.1*z + 0.7,
0.4*x + 0.6*y + 0.9*z + 0.5
]
sol_exact = {x:-88/175, y:-46/105, z:-1/25}
sol_linsolve = _linsolve(eqs, [x,y,z])
assert all_close(sol_exact, sol_linsolve)
eqs = [
0.4*x + 0.3*y + 0.6*z + 0.7,
0.4*x + 0.3*y + 0.9*z + 0.9,
0.7*x + 0.9*y,
]
sol_exact = {x:-9/5, y:7/5, z:-2/3}
sol_linsolve = _linsolve(eqs, [x,y,z])
assert all_close(sol_exact, sol_linsolve)
eqs = [
x*(0.7 + 0.6*I) + y*(0.4 + 0.7*I) + z*(0.9 + 0.1*I) + 0.5,
0.2*I*x + 0.2*I*y + z*(0.9 + 0.2*I) + 0.1,
x*(0.9 + 0.7*I) + y*(0.9 + 0.7*I) + z*(0.9 + 0.4*I) + 0.4,
]
sol_exact = {
x:-6157/7995 - 411/5330*I,
y:8519/15990 + 1784/7995*I,
z:-34/533 + 107/1599*I,
}
sol_linsolve = _linsolve(eqs, [x,y,z])
assert all_close(sol_exact, sol_linsolve)
# XXX: This system for x and y over RR(z) is problematic.
#
# eqs = [
# x*(0.2*z + 0.9) + y*(0.5*z + 0.8) + 0.6,
# 0.1*x*z + y*(0.1*z + 0.6) + 0.9,
# ]
#
# linsolve(eqs, [x, y])
# The solution for x comes out as
#
# -3.9e-5*z**2 - 3.6e-5*z - 8.67361737988404e-20
# x = ----------------------------------------------
# 3.0e-6*z**3 - 1.3e-5*z**2 - 5.4e-5*z
#
# The 8e-20 in the numerator should be zero which would allow z to cancel
# from top and bottom. It should be possible to avoid this somehow because
# the inverse of the matrix only has a quadratic factor (the determinant)
# in the denominator.
def test__linsolve_deprecated():
raises(PolyNonlinearError, lambda:
_linsolve([Eq(x**2, x**2 + y)], [x, y]))
raises(PolyNonlinearError, lambda:
_linsolve([(x + y)**2 - x**2], [x]))
raises(PolyNonlinearError, lambda:
_linsolve([Eq((x + y)**2, x**2)], [x]))

View File

@ -0,0 +1,145 @@
from sympy.polys.domains import ZZ, QQ
from sympy.polys.matrices import DM
from sympy.polys.matrices.domainmatrix import DomainMatrix
from sympy.polys.matrices.exceptions import DMRankError, DMValueError, DMShapeError, DMDomainError
from sympy.polys.matrices.lll import _ddm_lll, ddm_lll, ddm_lll_transform
from sympy.testing.pytest import raises
def test_lll():
normal_test_data = [
(
DM([[1, 0, 0, 0, -20160],
[0, 1, 0, 0, 33768],
[0, 0, 1, 0, 39578],
[0, 0, 0, 1, 47757]], ZZ),
DM([[10, -3, -2, 8, -4],
[3, -9, 8, 1, -11],
[-3, 13, -9, -3, -9],
[-12, -7, -11, 9, -1]], ZZ)
),
(
DM([[20, 52, 3456],
[14, 31, -1],
[34, -442, 0]], ZZ),
DM([[14, 31, -1],
[188, -101, -11],
[236, 13, 3443]], ZZ)
),
(
DM([[34, -1, -86, 12],
[-54, 34, 55, 678],
[23, 3498, 234, 6783],
[87, 49, 665, 11]], ZZ),
DM([[34, -1, -86, 12],
[291, 43, 149, 83],
[-54, 34, 55, 678],
[-189, 3077, -184, -223]], ZZ)
)
]
delta = QQ(5, 6)
for basis_dm, reduced_dm in normal_test_data:
reduced = _ddm_lll(basis_dm.rep.to_ddm(), delta=delta)[0]
assert reduced == reduced_dm.rep.to_ddm()
reduced = ddm_lll(basis_dm.rep.to_ddm(), delta=delta)
assert reduced == reduced_dm.rep.to_ddm()
reduced, transform = _ddm_lll(basis_dm.rep.to_ddm(), delta=delta, return_transform=True)
assert reduced == reduced_dm.rep.to_ddm()
assert transform.matmul(basis_dm.rep.to_ddm()) == reduced_dm.rep.to_ddm()
reduced, transform = ddm_lll_transform(basis_dm.rep.to_ddm(), delta=delta)
assert reduced == reduced_dm.rep.to_ddm()
assert transform.matmul(basis_dm.rep.to_ddm()) == reduced_dm.rep.to_ddm()
reduced = basis_dm.rep.lll(delta=delta)
assert reduced == reduced_dm.rep
reduced, transform = basis_dm.rep.lll_transform(delta=delta)
assert reduced == reduced_dm.rep
assert transform.matmul(basis_dm.rep) == reduced_dm.rep
reduced = basis_dm.rep.to_sdm().lll(delta=delta)
assert reduced == reduced_dm.rep.to_sdm()
reduced, transform = basis_dm.rep.to_sdm().lll_transform(delta=delta)
assert reduced == reduced_dm.rep.to_sdm()
assert transform.matmul(basis_dm.rep.to_sdm()) == reduced_dm.rep.to_sdm()
reduced = basis_dm.lll(delta=delta)
assert reduced == reduced_dm
reduced, transform = basis_dm.lll_transform(delta=delta)
assert reduced == reduced_dm
assert transform.matmul(basis_dm) == reduced_dm
def test_lll_linear_dependent():
linear_dependent_test_data = [
DM([[0, -1, -2, -3],
[1, 0, -1, -2],
[2, 1, 0, -1],
[3, 2, 1, 0]], ZZ),
DM([[1, 0, 0, 1],
[0, 1, 0, 1],
[0, 0, 1, 1],
[1, 2, 3, 6]], ZZ),
DM([[3, -5, 1],
[4, 6, 0],
[10, -4, 2]], ZZ)
]
for not_basis in linear_dependent_test_data:
raises(DMRankError, lambda: _ddm_lll(not_basis.rep.to_ddm()))
raises(DMRankError, lambda: ddm_lll(not_basis.rep.to_ddm()))
raises(DMRankError, lambda: not_basis.rep.lll())
raises(DMRankError, lambda: not_basis.rep.to_sdm().lll())
raises(DMRankError, lambda: not_basis.lll())
raises(DMRankError, lambda: _ddm_lll(not_basis.rep.to_ddm(), return_transform=True))
raises(DMRankError, lambda: ddm_lll_transform(not_basis.rep.to_ddm()))
raises(DMRankError, lambda: not_basis.rep.lll_transform())
raises(DMRankError, lambda: not_basis.rep.to_sdm().lll_transform())
raises(DMRankError, lambda: not_basis.lll_transform())
def test_lll_wrong_delta():
dummy_matrix = DomainMatrix.ones((3, 3), ZZ)
for wrong_delta in [QQ(-1, 4), QQ(0, 1), QQ(1, 4), QQ(1, 1), QQ(100, 1)]:
raises(DMValueError, lambda: _ddm_lll(dummy_matrix.rep, delta=wrong_delta))
raises(DMValueError, lambda: ddm_lll(dummy_matrix.rep, delta=wrong_delta))
raises(DMValueError, lambda: dummy_matrix.rep.lll(delta=wrong_delta))
raises(DMValueError, lambda: dummy_matrix.rep.to_sdm().lll(delta=wrong_delta))
raises(DMValueError, lambda: dummy_matrix.lll(delta=wrong_delta))
raises(DMValueError, lambda: _ddm_lll(dummy_matrix.rep, delta=wrong_delta, return_transform=True))
raises(DMValueError, lambda: ddm_lll_transform(dummy_matrix.rep, delta=wrong_delta))
raises(DMValueError, lambda: dummy_matrix.rep.lll_transform(delta=wrong_delta))
raises(DMValueError, lambda: dummy_matrix.rep.to_sdm().lll_transform(delta=wrong_delta))
raises(DMValueError, lambda: dummy_matrix.lll_transform(delta=wrong_delta))
def test_lll_wrong_shape():
wrong_shape_matrix = DomainMatrix.ones((4, 3), ZZ)
raises(DMShapeError, lambda: _ddm_lll(wrong_shape_matrix.rep))
raises(DMShapeError, lambda: ddm_lll(wrong_shape_matrix.rep))
raises(DMShapeError, lambda: wrong_shape_matrix.rep.lll())
raises(DMShapeError, lambda: wrong_shape_matrix.rep.to_sdm().lll())
raises(DMShapeError, lambda: wrong_shape_matrix.lll())
raises(DMShapeError, lambda: _ddm_lll(wrong_shape_matrix.rep, return_transform=True))
raises(DMShapeError, lambda: ddm_lll_transform(wrong_shape_matrix.rep))
raises(DMShapeError, lambda: wrong_shape_matrix.rep.lll_transform())
raises(DMShapeError, lambda: wrong_shape_matrix.rep.to_sdm().lll_transform())
raises(DMShapeError, lambda: wrong_shape_matrix.lll_transform())
def test_lll_wrong_domain():
wrong_domain_matrix = DomainMatrix.ones((3, 3), QQ)
raises(DMDomainError, lambda: _ddm_lll(wrong_domain_matrix.rep))
raises(DMDomainError, lambda: ddm_lll(wrong_domain_matrix.rep))
raises(DMDomainError, lambda: wrong_domain_matrix.rep.lll())
raises(DMDomainError, lambda: wrong_domain_matrix.rep.to_sdm().lll())
raises(DMDomainError, lambda: wrong_domain_matrix.lll())
raises(DMDomainError, lambda: _ddm_lll(wrong_domain_matrix.rep, return_transform=True))
raises(DMDomainError, lambda: ddm_lll_transform(wrong_domain_matrix.rep))
raises(DMDomainError, lambda: wrong_domain_matrix.rep.lll_transform())
raises(DMDomainError, lambda: wrong_domain_matrix.rep.to_sdm().lll_transform())
raises(DMDomainError, lambda: wrong_domain_matrix.lll_transform())

View File

@ -0,0 +1,75 @@
from sympy.testing.pytest import raises
from sympy.core.symbol import Symbol
from sympy.polys.matrices.normalforms import (
invariant_factors, smith_normal_form,
hermite_normal_form, _hermite_normal_form, _hermite_normal_form_modulo_D)
from sympy.polys.domains import ZZ, QQ
from sympy.polys.matrices import DomainMatrix, DM
from sympy.polys.matrices.exceptions import DMDomainError, DMShapeError
def test_smith_normal():
m = DM([[12, 6, 4, 8], [3, 9, 6, 12], [2, 16, 14, 28], [20, 10, 10, 20]], ZZ)
smf = DM([[1, 0, 0, 0], [0, 10, 0, 0], [0, 0, -30, 0], [0, 0, 0, 0]], ZZ)
assert smith_normal_form(m).to_dense() == smf
x = Symbol('x')
m = DM([[x-1, 1, -1],
[ 0, x, -1],
[ 0, -1, x]], QQ[x])
dx = m.domain.gens[0]
assert invariant_factors(m) == (1, dx-1, dx**2-1)
zr = DomainMatrix([], (0, 2), ZZ)
zc = DomainMatrix([[], []], (2, 0), ZZ)
assert smith_normal_form(zr).to_dense() == zr
assert smith_normal_form(zc).to_dense() == zc
assert smith_normal_form(DM([[2, 4]], ZZ)).to_dense() == DM([[2, 0]], ZZ)
assert smith_normal_form(DM([[0, -2]], ZZ)).to_dense() == DM([[-2, 0]], ZZ)
assert smith_normal_form(DM([[0], [-2]], ZZ)).to_dense() == DM([[-2], [0]], ZZ)
m = DM([[3, 0, 0, 0], [0, 0, 0, 0], [0, 0, 2, 0]], ZZ)
snf = DM([[1, 0, 0, 0], [0, 6, 0, 0], [0, 0, 0, 0]], ZZ)
assert smith_normal_form(m).to_dense() == snf
raises(ValueError, lambda: smith_normal_form(DM([[1]], ZZ[x])))
def test_hermite_normal():
m = DM([[2, 7, 17, 29, 41], [3, 11, 19, 31, 43], [5, 13, 23, 37, 47]], ZZ)
hnf = DM([[1, 0, 0], [0, 2, 1], [0, 0, 1]], ZZ)
assert hermite_normal_form(m) == hnf
assert hermite_normal_form(m, D=ZZ(2)) == hnf
assert hermite_normal_form(m, D=ZZ(2), check_rank=True) == hnf
m = m.transpose()
hnf = DM([[37, 0, 19], [222, -6, 113], [48, 0, 25], [0, 2, 1], [0, 0, 1]], ZZ)
assert hermite_normal_form(m) == hnf
raises(DMShapeError, lambda: _hermite_normal_form_modulo_D(m, ZZ(96)))
raises(DMDomainError, lambda: _hermite_normal_form_modulo_D(m, QQ(96)))
m = DM([[8, 28, 68, 116, 164], [3, 11, 19, 31, 43], [5, 13, 23, 37, 47]], ZZ)
hnf = DM([[4, 0, 0], [0, 2, 1], [0, 0, 1]], ZZ)
assert hermite_normal_form(m) == hnf
assert hermite_normal_form(m, D=ZZ(8)) == hnf
assert hermite_normal_form(m, D=ZZ(8), check_rank=True) == hnf
m = DM([[10, 8, 6, 30, 2], [45, 36, 27, 18, 9], [5, 4, 3, 2, 1]], ZZ)
hnf = DM([[26, 2], [0, 9], [0, 1]], ZZ)
assert hermite_normal_form(m) == hnf
m = DM([[2, 7], [0, 0], [0, 0]], ZZ)
hnf = DM([[1], [0], [0]], ZZ)
assert hermite_normal_form(m) == hnf
m = DM([[-2, 1], [0, 1]], ZZ)
hnf = DM([[2, 1], [0, 1]], ZZ)
assert hermite_normal_form(m) == hnf
m = DomainMatrix([[QQ(1)]], (1, 1), QQ)
raises(DMDomainError, lambda: hermite_normal_form(m))
raises(DMDomainError, lambda: _hermite_normal_form(m))
raises(DMDomainError, lambda: _hermite_normal_form_modulo_D(m, ZZ(1)))

View File

@ -0,0 +1,209 @@
from sympy import ZZ, Matrix
from sympy.polys.matrices import DM, DomainMatrix
from sympy.polys.matrices.ddm import DDM
from sympy.polys.matrices.sdm import SDM
import pytest
zeros = lambda shape, K: DomainMatrix.zeros(shape, K).to_dense()
eye = lambda n, K: DomainMatrix.eye(n, K).to_dense()
#
# DomainMatrix.nullspace can have a divided answer or can return an undivided
# uncanonical answer. The uncanonical answer is not unique but we can make it
# unique by making it primitive (remove gcd). The tests here all show the
# primitive form. We test two things:
#
# A.nullspace().primitive()[1] == answer.
# A.nullspace(divide_last=True) == _divide_last(answer).
#
# The nullspace as returned by DomainMatrix and related classes is the
# transpose of the nullspace as returned by Matrix. Matrix returns a list of
# of column vectors whereas DomainMatrix returns a matrix whose rows are the
# nullspace vectors.
#
NULLSPACE_EXAMPLES = [
(
'zz_1',
DM([[ 1, 2, 3]], ZZ),
DM([[-2, 1, 0],
[-3, 0, 1]], ZZ),
),
(
'zz_2',
zeros((0, 0), ZZ),
zeros((0, 0), ZZ),
),
(
'zz_3',
zeros((2, 0), ZZ),
zeros((0, 0), ZZ),
),
(
'zz_4',
zeros((0, 2), ZZ),
eye(2, ZZ),
),
(
'zz_5',
zeros((2, 2), ZZ),
eye(2, ZZ),
),
(
'zz_6',
DM([[1, 2],
[3, 4]], ZZ),
zeros((0, 2), ZZ),
),
(
'zz_7',
DM([[1, 1],
[1, 1]], ZZ),
DM([[-1, 1]], ZZ),
),
(
'zz_8',
DM([[1],
[1]], ZZ),
zeros((0, 1), ZZ),
),
(
'zz_9',
DM([[1, 1]], ZZ),
DM([[-1, 1]], ZZ),
),
(
'zz_10',
DM([[0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 1, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 1, 0, 0, 0, 0, 1]], ZZ),
DM([[ 0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
[-1, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[ 0, -1, 0, 0, 0, 0, 0, 1, 0, 0],
[ 0, 0, 0, -1, 0, 0, 0, 0, 1, 0],
[ 0, 0, 0, 0, -1, 0, 0, 0, 0, 1]], ZZ),
),
]
def _to_DM(A, ans):
"""Convert the answer to DomainMatrix."""
if isinstance(A, DomainMatrix):
return A.to_dense()
elif isinstance(A, DDM):
return DomainMatrix(list(A), A.shape, A.domain).to_dense()
elif isinstance(A, SDM):
return DomainMatrix(dict(A), A.shape, A.domain).to_dense()
else:
assert False # pragma: no cover
def _divide_last(null):
"""Normalize the nullspace by the rightmost non-zero entry."""
null = null.to_field()
if null.is_zero_matrix:
return null
rows = []
for i in range(null.shape[0]):
for j in reversed(range(null.shape[1])):
if null[i, j]:
rows.append(null[i, :] / null[i, j])
break
else:
assert False # pragma: no cover
return DomainMatrix.vstack(*rows)
def _check_primitive(null, null_ans):
"""Check that the primitive of the answer matches."""
null = _to_DM(null, null_ans)
cont, null_prim = null.primitive()
assert null_prim == null_ans
def _check_divided(null, null_ans):
"""Check the divided answer."""
null = _to_DM(null, null_ans)
null_ans_norm = _divide_last(null_ans)
assert null == null_ans_norm
@pytest.mark.parametrize('name, A, A_null', NULLSPACE_EXAMPLES)
def test_Matrix_nullspace(name, A, A_null):
A = A.to_Matrix()
A_null_cols = A.nullspace()
# We have to patch up the case where the nullspace is empty
if A_null_cols:
A_null_found = Matrix.hstack(*A_null_cols)
else:
A_null_found = Matrix.zeros(A.cols, 0)
A_null_found = A_null_found.to_DM().to_field().to_dense()
# The Matrix result is the transpose of DomainMatrix result.
A_null_found = A_null_found.transpose()
_check_divided(A_null_found, A_null)
@pytest.mark.parametrize('name, A, A_null', NULLSPACE_EXAMPLES)
def test_dm_dense_nullspace(name, A, A_null):
A = A.to_field().to_dense()
A_null_found = A.nullspace(divide_last=True)
_check_divided(A_null_found, A_null)
@pytest.mark.parametrize('name, A, A_null', NULLSPACE_EXAMPLES)
def test_dm_sparse_nullspace(name, A, A_null):
A = A.to_field().to_sparse()
A_null_found = A.nullspace(divide_last=True)
_check_divided(A_null_found, A_null)
@pytest.mark.parametrize('name, A, A_null', NULLSPACE_EXAMPLES)
def test_ddm_nullspace(name, A, A_null):
A = A.to_field().to_ddm()
A_null_found, _ = A.nullspace()
_check_divided(A_null_found, A_null)
@pytest.mark.parametrize('name, A, A_null', NULLSPACE_EXAMPLES)
def test_sdm_nullspace(name, A, A_null):
A = A.to_field().to_sdm()
A_null_found, _ = A.nullspace()
_check_divided(A_null_found, A_null)
@pytest.mark.parametrize('name, A, A_null', NULLSPACE_EXAMPLES)
def test_dm_dense_nullspace_fracfree(name, A, A_null):
A = A.to_dense()
A_null_found = A.nullspace()
_check_primitive(A_null_found, A_null)
@pytest.mark.parametrize('name, A, A_null', NULLSPACE_EXAMPLES)
def test_dm_sparse_nullspace_fracfree(name, A, A_null):
A = A.to_sparse()
A_null_found = A.nullspace()
_check_primitive(A_null_found, A_null)

View File

@ -0,0 +1,737 @@
from sympy import ZZ, QQ, ZZ_I, EX, Matrix, eye, zeros, symbols
from sympy.polys.matrices import DM, DomainMatrix
from sympy.polys.matrices.dense import ddm_irref_den, ddm_irref
from sympy.polys.matrices.ddm import DDM
from sympy.polys.matrices.sdm import SDM, sdm_irref, sdm_rref_den
import pytest
#
# The dense and sparse implementations of rref_den are ddm_irref_den and
# sdm_irref_den. These can give results that differ by some factor and also
# give different results if the order of the rows is changed. The tests below
# show all results on lowest terms as should be returned by cancel_denom.
#
# The EX domain is also a case where the dense and sparse implementations
# can give results in different forms: the results should be equivalent but
# are not canonical because EX does not have a canonical form.
#
a, b, c, d = symbols('a, b, c, d')
qq_large_1 = DM([
[ (1,2), (1,3), (1,5), (1,7), (1,11), (1,13), (1,17), (1,19), (1,23), (1,29), (1,31)],
[ (1,37), (1,41), (1,43), (1,47), (1,53), (1,59), (1,61), (1,67), (1,71), (1,73), (1,79)],
[ (1,83), (1,89), (1,97),(1,101),(1,103),(1,107),(1,109),(1,113),(1,127),(1,131),(1,137)],
[(1,139),(1,149),(1,151),(1,157),(1,163),(1,167),(1,173),(1,179),(1,181),(1,191),(1,193)],
[(1,197),(1,199),(1,211),(1,223),(1,227),(1,229),(1,233),(1,239),(1,241),(1,251),(1,257)],
[(1,263),(1,269),(1,271),(1,277),(1,281),(1,283),(1,293),(1,307),(1,311),(1,313),(1,317)],
[(1,331),(1,337),(1,347),(1,349),(1,353),(1,359),(1,367),(1,373),(1,379),(1,383),(1,389)],
[(1,397),(1,401),(1,409),(1,419),(1,421),(1,431),(1,433),(1,439),(1,443),(1,449),(1,457)],
[(1,461),(1,463),(1,467),(1,479),(1,487),(1,491),(1,499),(1,503),(1,509),(1,521),(1,523)],
[(1,541),(1,547),(1,557),(1,563),(1,569),(1,571),(1,577),(1,587),(1,593),(1,599),(1,601)],
[(1,607),(1,613),(1,617),(1,619),(1,631),(1,641),(1,643),(1,647),(1,653),(1,659),(1,661)]],
QQ)
qq_large_2 = qq_large_1 + 10**100 * DomainMatrix.eye(11, QQ)
RREF_EXAMPLES = [
(
'zz_1',
DM([[1, 2, 3]], ZZ),
DM([[1, 2, 3]], ZZ),
ZZ(1),
),
(
'zz_2',
DomainMatrix([], (0, 0), ZZ),
DomainMatrix([], (0, 0), ZZ),
ZZ(1),
),
(
'zz_3',
DM([[1, 2],
[3, 4]], ZZ),
DM([[1, 0],
[0, 1]], ZZ),
ZZ(1),
),
(
'zz_4',
DM([[1, 0],
[3, 4]], ZZ),
DM([[1, 0],
[0, 1]], ZZ),
ZZ(1),
),
(
'zz_5',
DM([[0, 2],
[3, 4]], ZZ),
DM([[1, 0],
[0, 1]], ZZ),
ZZ(1),
),
(
'zz_6',
DM([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]], ZZ),
DM([[1, 0, -1],
[0, 1, 2],
[0, 0, 0]], ZZ),
ZZ(1),
),
(
'zz_7',
DM([[0, 0, 0],
[0, 0, 0],
[1, 0, 0]], ZZ),
DM([[1, 0, 0],
[0, 0, 0],
[0, 0, 0]], ZZ),
ZZ(1),
),
(
'zz_8',
DM([[0, 0, 0],
[0, 0, 0],
[0, 0, 0]], ZZ),
DM([[0, 0, 0],
[0, 0, 0],
[0, 0, 0]], ZZ),
ZZ(1),
),
(
'zz_9',
DM([[1, 1, 0],
[0, 0, 2],
[0, 0, 0]], ZZ),
DM([[1, 1, 0],
[0, 0, 1],
[0, 0, 0]], ZZ),
ZZ(1),
),
(
'zz_10',
DM([[2, 2, 0],
[0, 0, 2],
[0, 0, 0]], ZZ),
DM([[1, 1, 0],
[0, 0, 1],
[0, 0, 0]], ZZ),
ZZ(1),
),
(
'zz_11',
DM([[2, 2, 0],
[0, 2, 2],
[0, 0, 2]], ZZ),
DM([[1, 0, 0],
[0, 1, 0],
[0, 0, 1]], ZZ),
ZZ(1),
),
(
'zz_12',
DM([[ 1, 2, 3],
[ 4, 5, 6],
[ 7, 8, 9],
[10, 11, 12]], ZZ),
DM([[1, 0, -1],
[0, 1, 2],
[0, 0, 0],
[0, 0, 0]], ZZ),
ZZ(1),
),
(
'zz_13',
DM([[ 1, 2, 3],
[ 4, 5, 6],
[ 7, 8, 9],
[10, 11, 13]], ZZ),
DM([[ 1, 0, 0],
[ 0, 1, 0],
[ 0, 0, 1],
[ 0, 0, 0]], ZZ),
ZZ(1),
),
(
'zz_14',
DM([[1, 2, 4, 3],
[4, 5, 10, 6],
[7, 8, 16, 9]], ZZ),
DM([[1, 0, 0, -1],
[0, 1, 2, 2],
[0, 0, 0, 0]], ZZ),
ZZ(1),
),
(
'zz_15',
DM([[1, 2, 4, 3],
[4, 5, 10, 6],
[7, 8, 17, 9]], ZZ),
DM([[1, 0, 0, -1],
[0, 1, 0, 2],
[0, 0, 1, 0]], ZZ),
ZZ(1),
),
(
'zz_16',
DM([[1, 2, 0, 1],
[1, 1, 9, 0]], ZZ),
DM([[1, 0, 18, -1],
[0, 1, -9, 1]], ZZ),
ZZ(1),
),
(
'zz_17',
DM([[1, 1, 1],
[1, 2, 2]], ZZ),
DM([[1, 0, 0],
[0, 1, 1]], ZZ),
ZZ(1),
),
(
# Here the sparse implementation and dense implementation give very
# different denominators: 4061232 and -1765176.
'zz_18',
DM([[94, 24, 0, 27, 0],
[79, 0, 0, 0, 0],
[85, 16, 71, 81, 0],
[ 0, 0, 72, 77, 0],
[21, 0, 34, 0, 0]], ZZ),
DM([[ 1, 0, 0, 0, 0],
[ 0, 1, 0, 0, 0],
[ 0, 0, 1, 0, 0],
[ 0, 0, 0, 1, 0],
[ 0, 0, 0, 0, 0]], ZZ),
ZZ(1),
),
(
# Let's have a denominator that cannot be cancelled.
'zz_19',
DM([[1, 2, 4],
[4, 5, 6]], ZZ),
DM([[3, 0, -8],
[0, 3, 10]], ZZ),
ZZ(3),
),
(
'zz_20',
DM([[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 4]], ZZ),
DM([[0, 0, 0, 0, 1],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]], ZZ),
ZZ(1),
),
(
'zz_21',
DM([[0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 1, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 1, 0, 0, 0, 0, 1]], ZZ),
DM([[1, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 1, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0]], ZZ),
ZZ(1),
),
(
'zz_22',
DM([[1, 1, 1, 0, 1],
[1, 1, 0, 1, 0],
[1, 0, 1, 0, 1],
[1, 1, 0, 1, 0],
[1, 0, 0, 0, 0]], ZZ),
DM([[1, 0, 0, 0, 0],
[0, 1, 0, 0, 0],
[0, 0, 1, 0, 1],
[0, 0, 0, 1, 0],
[0, 0, 0, 0, 0]], ZZ),
ZZ(1),
),
(
'zz_large_1',
DM([
[ 0, 0, 0, 81, 0, 0, 75, 0, 0, 0, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0, 86, 0, 92, 79, 54, 0, 7, 0, 0, 0, 0, 79, 0, 0, 0],
[89, 54, 81, 0, 0, 20, 0, 0, 0, 0, 0, 0, 51, 0, 94, 0, 0, 77, 0, 0],
[ 0, 0, 0, 96, 0, 0, 0, 0, 0, 0, 0, 0, 48, 29, 0, 0, 5, 0, 32, 0],
[ 0, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 60, 0, 0, 0, 11],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 37, 0, 43, 0, 0],
[ 0, 0, 0, 0, 0, 38, 91, 0, 0, 0, 0, 38, 0, 0, 0, 0, 0, 26, 0, 0],
[69, 0, 0, 0, 0, 0, 94, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 55],
[ 0, 13, 18, 49, 49, 88, 0, 0, 35, 54, 0, 0, 51, 0, 0, 0, 0, 0, 0, 87],
[ 0, 0, 0, 0, 31, 0, 40, 0, 0, 0, 0, 0, 0, 50, 0, 0, 0, 0, 88, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 98, 0, 0, 0, 15, 53, 0, 92, 0, 0, 0, 0],
[ 0, 0, 0, 95, 0, 0, 0, 36, 0, 0, 0, 0, 0, 72, 0, 0, 0, 0, 73, 19],
[ 0, 65, 14, 96, 0, 0, 0, 0, 0, 0, 0, 0, 0, 90, 0, 0, 0, 34, 0, 0],
[ 0, 0, 0, 16, 39, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51, 0, 0],
[ 0, 17, 0, 0, 0, 99, 84, 13, 50, 84, 0, 0, 0, 0, 95, 0, 43, 33, 20, 0],
[79, 0, 17, 52, 99, 12, 69, 0, 98, 0, 68, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[ 0, 0, 0, 82, 0, 44, 0, 0, 0, 97, 0, 0, 0, 0, 0, 10, 0, 0, 31, 0],
[ 0, 0, 21, 0, 67, 0, 0, 0, 0, 0, 4, 0, 50, 0, 0, 0, 33, 0, 0, 0],
[ 0, 0, 0, 0, 9, 42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8],
[ 0, 77, 0, 0, 0, 0, 0, 0, 0, 0, 34, 93, 0, 0, 0, 0, 47, 0, 0, 0]],
ZZ),
DM([[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]], ZZ),
ZZ(1),
),
(
'zz_large_2',
DM([
[ 0, 0, 0, 0, 50, 0, 6, 81, 0, 1, 86, 0, 0, 98, 82, 94, 4, 0, 0, 29],
[ 0, 44, 43, 0, 62, 0, 0, 0, 60, 0, 0, 0, 0, 71, 9, 0, 57, 41, 0, 93],
[ 0, 0, 28, 0, 74, 89, 42, 0, 28, 0, 6, 0, 0, 0, 44, 0, 0, 0, 77, 19],
[ 0, 21, 82, 0, 30, 88, 0, 89, 68, 0, 0, 0, 79, 41, 0, 0, 99, 0, 0, 0],
[31, 0, 0, 0, 19, 64, 0, 0, 79, 0, 5, 0, 72, 10, 60, 32, 64, 59, 0, 24],
[ 0, 0, 0, 0, 0, 57, 0, 94, 0, 83, 20, 0, 0, 9, 31, 0, 49, 26, 58, 0],
[ 0, 65, 56, 31, 64, 0, 0, 0, 0, 0, 0, 52, 85, 0, 0, 0, 0, 51, 0, 0],
[ 0, 35, 0, 0, 0, 69, 0, 0, 64, 0, 0, 0, 0, 70, 0, 0, 90, 0, 75, 76],
[69, 7, 0, 90, 0, 0, 84, 0, 47, 69, 19, 20, 42, 0, 0, 32, 71, 35, 0, 0],
[39, 0, 90, 0, 0, 4, 85, 0, 0, 55, 0, 0, 0, 35, 67, 40, 0, 40, 0, 77],
[98, 63, 0, 71, 0, 50, 0, 2, 61, 0, 38, 0, 0, 0, 0, 75, 0, 40, 33, 56],
[ 0, 73, 0, 64, 0, 38, 0, 35, 61, 0, 0, 52, 0, 7, 0, 51, 0, 0, 0, 34],
[ 0, 0, 28, 0, 34, 5, 63, 45, 14, 42, 60, 16, 76, 54, 99, 0, 28, 30, 0, 0],
[58, 37, 14, 0, 0, 0, 94, 0, 0, 90, 0, 0, 0, 0, 0, 0, 0, 8, 90, 53],
[86, 74, 94, 0, 49, 10, 60, 0, 40, 18, 0, 0, 0, 31, 60, 24, 0, 1, 0, 29],
[53, 0, 0, 97, 0, 0, 58, 0, 0, 39, 44, 47, 0, 0, 0, 12, 50, 0, 0, 11],
[ 4, 0, 92, 10, 28, 0, 0, 89, 0, 0, 18, 54, 23, 39, 0, 2, 0, 48, 0, 92],
[ 0, 0, 90, 77, 95, 33, 0, 0, 49, 22, 39, 0, 0, 0, 0, 0, 0, 40, 0, 0],
[96, 0, 0, 0, 0, 38, 86, 0, 22, 76, 0, 0, 0, 0, 83, 88, 95, 65, 72, 0],
[81, 65, 0, 4, 60, 0, 19, 0, 0, 68, 0, 0, 89, 0, 67, 22, 0, 0, 55, 33]],
ZZ),
DM([
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]],
ZZ),
ZZ(1),
),
(
'zz_large_3',
DM([
[62,35,89,58,22,47,30,28,52,72,17,56,80,26,64,21,10,35,24,42,96,32,23,50,92,37,76,94,63,66],
[20,47,96,34,10,98,19,6,29,2,19,92,61,94,38,41,32,9,5,94,31,58,27,41,72,85,61,62,40,46],
[69,26,35,68,25,52,94,13,38,65,81,10,29,15,5,4,13,99,85,0,80,51,60,60,26,77,85,2,87,25],
[99,58,69,15,52,12,18,7,27,56,12,54,21,92,38,95,33,83,28,1,44,8,29,84,92,12,2,25,46,46],
[93,13,55,48,35,87,24,40,23,35,25,32,0,19,0,85,4,79,26,11,46,75,7,96,76,11,7,57,99,75],
[128,85,26,51,161,173,77,78,85,103,123,58,91,147,38,91,161,36,123,81,102,25,75,59,17,150,112,65,77,143],
[15,59,61,82,12,83,34,8,94,71,66,7,91,21,48,69,26,12,64,38,97,87,38,15,51,33,93,43,66,89],
[74,74,53,39,69,90,41,80,32,66,40,83,87,87,61,38,12,80,24,49,37,90,19,33,56,0,46,57,56,60],
[82,11,0,25,56,58,39,49,92,93,80,38,19,62,33,85,19,61,14,30,45,91,97,34,97,53,92,28,33,43],
[83,79,41,16,95,35,53,45,26,4,71,76,61,69,69,72,87,92,59,72,54,11,22,83,8,57,77,55,19,22],
[49,34,13,31,72,77,52,70,46,41,37,6,42,66,35,6,75,33,62,57,30,14,26,31,9,95,89,13,12,90],
[29,3,49,30,51,32,77,41,38,50,16,1,87,81,93,88,58,91,83,0,38,67,29,64,60,84,5,60,23,28],
[79,51,13,20,89,96,25,8,39,62,86,52,49,81,3,85,86,3,61,24,72,11,49,28,8,55,23,52,65,53],
[96,86,73,20,41,20,37,18,10,61,85,24,40,83,69,41,4,92,23,99,64,33,18,36,32,56,60,98,39,24],
[32,62,47,80,51,66,17,1,9,30,65,75,75,88,99,92,64,53,53,86,38,51,41,14,35,18,39,25,26,32],
[39,21,8,16,33,6,35,85,75,62,43,34,18,68,71,28,32,18,12,0,81,53,1,99,3,5,45,99,35,33],
[19,95,89,45,75,94,92,5,84,93,34,17,50,56,79,98,68,82,65,81,51,90,5,95,33,71,46,61,14,7],
[53,92,8,49,67,84,21,79,49,95,66,48,36,14,62,97,26,45,58,31,83,48,11,89,67,72,91,34,56,89],
[56,76,99,92,40,8,0,16,15,48,35,72,91,46,81,14,86,60,51,7,33,12,53,78,48,21,3,89,15,79],
[81,43,33,49,6,49,36,32,57,74,87,91,17,37,31,17,67,1,40,38,69,8,3,48,59,37,64,97,11,3],
[98,48,77,16,2,48,57,38,63,59,79,35,16,71,60,86,71,41,14,76,80,97,77,69,4,58,22,55,26,73],
[80,47,78,44,31,48,47,29,29,62,19,21,17,24,19,3,53,93,97,57,13,54,12,10,77,66,60,75,32,21],
[86,63,2,13,71,38,86,23,18,15,91,65,77,65,9,92,50,0,17,42,99,80,99,27,10,99,92,9,87,84],
[66,27,72,13,13,15,72,75,39,3,14,71,15,68,10,19,49,54,11,29,47,20,63,13,97,47,24,62,16,96],
[42,63,83,60,49,68,9,53,75,87,40,25,12,63,0,12,0,95,46,46,55,25,89,1,51,1,1,96,80,52],
[35,9,97,13,86,39,66,48,41,57,23,38,11,9,35,72,88,13,41,60,10,64,71,23,1,5,23,57,6,19],
[70,61,5,50,72,60,77,13,41,94,1,45,52,22,99,47,27,18,99,42,16,48,26,9,88,77,10,94,11,92],
[55,68,58,2,72,56,81,52,79,37,1,40,21,46,27,60,37,13,97,42,85,98,69,60,76,44,42,46,29,73],
[73,0,43,17,89,97,45,2,68,14,55,60,95,2,74,85,88,68,93,76,38,76,2,51,45,76,50,79,56,18],
[72,58,41,39,24,80,23,79,44,7,98,75,30,6,85,60,20,58,77,71,90,51,38,80,30,15,33,10,82,8]],
ZZ),
Matrix([
[eye(29) * 2028539767964472550625641331179545072876560857886207583101,
Matrix([ 4260575808093245475167216057435155595594339172099000182569,
169148395880755256182802335904188369274227936894862744452,
4915975976683942569102447281579134986891620721539038348914,
6113916866367364958834844982578214901958429746875633283248,
5585689617819894460378537031623265659753379011388162534838,
359776822829880747716695359574308645968094838905181892423,
-2800926112141776386671436511182421432449325232461665113305,
941642292388230001722444876624818265766384442910688463158,
3648811843256146649321864698600908938933015862008642023935,
-4104526163246702252932955226754097174212129127510547462419,
-704814955438106792441896903238080197619233342348191408078,
1640882266829725529929398131287244562048075707575030019335,
-4068330845192910563212155694231438198040299927120544468520,
136589038308366497790495711534532612862715724187671166593,
2544937011460702462290799932536905731142196510605191645593,
755591839174293940486133926192300657264122907519174116472,
-3683838489869297144348089243628436188645897133242795965021,
-522207137101161299969706310062775465103537953077871128403,
-2260451796032703984456606059649402832441331339246756656334,
-6476809325293587953616004856993300606040336446656916663680,
3521944238996782387785653800944972787867472610035040989081,
2270762115788407950241944504104975551914297395787473242379,
-3259947194628712441902262570532921252128444706733549251156,
-5624569821491886970999097239695637132075823246850431083557,
-3262698255682055804320585332902837076064075936601504555698,
5786719943788937667411185880136324396357603606944869545501,
-955257841973865996077323863289453200904051299086000660036,
-1294235552446355326174641248209752679127075717918392702116,
-3718353510747301598130831152458342785269166356215331448279,
]),],
[zeros(1, 29), zeros(1, 1)],
]).to_DM().to_dense(),
ZZ(2028539767964472550625641331179545072876560857886207583101),
),
(
'qq_1',
DM([[(1,2), 0], [0, 2]], QQ),
DM([[1, 0], [0, 1]], QQ),
QQ(1),
),
(
# Standard square case
'qq_2',
DM([[0, 1],
[1, 1]], QQ),
DM([[1, 0],
[0, 1]], QQ),
QQ(1),
),
(
# m < n case
'qq_3',
DM([[1, 2, 1],
[3, 4, 1]], QQ),
DM([[1, 0, -1],
[0, 1, 1]], QQ),
QQ(1),
),
(
# same m < n but reversed
'qq_4',
DM([[3, 4, 1],
[1, 2, 1]], QQ),
DM([[1, 0, -1],
[0, 1, 1]], QQ),
QQ(1),
),
(
# m > n case
'qq_5',
DM([[1, 0],
[1, 3],
[0, 1]], QQ),
DM([[1, 0],
[0, 1],
[0, 0]], QQ),
QQ(1),
),
(
# Example with missing pivot
'qq_6',
DM([[1, 0, 1],
[3, 0, 1]], QQ),
DM([[1, 0, 0],
[0, 0, 1]], QQ),
QQ(1),
),
(
# This is intended to trigger the threshold where we give up on
# clearing denominators.
'qq_large_1',
qq_large_1,
DomainMatrix.eye(11, QQ).to_dense(),
QQ(1),
),
(
# This is intended to trigger the threshold where we use rref_den over
# QQ.
'qq_large_2',
qq_large_2,
DomainMatrix.eye(11, QQ).to_dense(),
QQ(1),
),
(
# Example with missing pivot and no replacement
# This example is just enough to show a different result from the dense
# and sparse versions of the algorithm:
#
# >>> A = Matrix([[0, 1], [0, 2], [1, 0]])
# >>> A.to_DM().to_sparse().rref_den()[0].to_Matrix()
# Matrix([
# [1, 0],
# [0, 1],
# [0, 0]])
# >>> A.to_DM().to_dense().rref_den()[0].to_Matrix()
# Matrix([
# [2, 0],
# [0, 2],
# [0, 0]])
#
'qq_7',
DM([[0, 1],
[0, 2],
[1, 0]], QQ),
DM([[1, 0],
[0, 1],
[0, 0]], QQ),
QQ(1),
),
(
# Gaussian integers
'zz_i_1',
DM([[(0,1), 1, 1],
[ 1, 1, 1]], ZZ_I),
DM([[1, 0, 0],
[0, 1, 1]], ZZ_I),
ZZ_I(1),
),
(
# EX: test_issue_23718
'EX_1',
DM([
[a, b, 1],
[c, d, 1]], EX),
DM([[a*d - b*c, 0, -b + d],
[ 0, a*d - b*c, a - c]], EX),
EX(a*d - b*c),
),
]
def _to_DM(A, ans):
"""Convert the answer to DomainMatrix."""
if isinstance(A, DomainMatrix):
return A.to_dense()
elif isinstance(A, Matrix):
return A.to_DM(ans.domain).to_dense()
if not (hasattr(A, 'shape') and hasattr(A, 'domain')):
shape, domain = ans.shape, ans.domain
else:
shape, domain = A.shape, A.domain
if isinstance(A, (DDM, list)):
return DomainMatrix(list(A), shape, domain).to_dense()
elif isinstance(A, (SDM, dict)):
return DomainMatrix(dict(A), shape, domain).to_dense()
else:
assert False # pragma: no cover
def _pivots(A_rref):
"""Return the pivots from the rref of A."""
return tuple(sorted(map(min, A_rref.to_sdm().values())))
def _check_cancel(result, rref_ans, den_ans):
"""Check the cancelled result."""
rref, den, pivots = result
if isinstance(rref, (DDM, SDM, list, dict)):
assert type(pivots) is list
pivots = tuple(pivots)
rref = _to_DM(rref, rref_ans)
rref2, den2 = rref.cancel_denom(den)
assert rref2 == rref_ans
assert den2 == den_ans
assert pivots == _pivots(rref)
def _check_divide(result, rref_ans, den_ans):
"""Check the divided result."""
rref, pivots = result
if isinstance(rref, (DDM, SDM, list, dict)):
assert type(pivots) is list
pivots = tuple(pivots)
rref_ans = rref_ans.to_field() / den_ans
rref = _to_DM(rref, rref_ans)
assert rref == rref_ans
assert _pivots(rref) == pivots
@pytest.mark.parametrize('name, A, A_rref, den', RREF_EXAMPLES)
def test_Matrix_rref(name, A, A_rref, den):
K = A.domain
A = A.to_Matrix()
A_rref_found, pivots = A.rref()
if K.is_EX:
A_rref_found = A_rref_found.expand()
_check_divide((A_rref_found, pivots), A_rref, den)
@pytest.mark.parametrize('name, A, A_rref, den', RREF_EXAMPLES)
def test_dm_dense_rref(name, A, A_rref, den):
A = A.to_field()
_check_divide(A.rref(), A_rref, den)
@pytest.mark.parametrize('name, A, A_rref, den', RREF_EXAMPLES)
def test_dm_dense_rref_den(name, A, A_rref, den):
_check_cancel(A.rref_den(), A_rref, den)
@pytest.mark.parametrize('name, A, A_rref, den', RREF_EXAMPLES)
def test_dm_sparse_rref(name, A, A_rref, den):
A = A.to_field().to_sparse()
_check_divide(A.rref(), A_rref, den)
@pytest.mark.parametrize('name, A, A_rref, den', RREF_EXAMPLES)
def test_dm_sparse_rref_den(name, A, A_rref, den):
A = A.to_sparse()
_check_cancel(A.rref_den(), A_rref, den)
@pytest.mark.parametrize('name, A, A_rref, den', RREF_EXAMPLES)
def test_dm_sparse_rref_den_keep_domain(name, A, A_rref, den):
A = A.to_sparse()
A_rref_f, den_f, pivots_f = A.rref_den(keep_domain=False)
A_rref_f = A_rref_f.to_field() / den_f
_check_divide((A_rref_f, pivots_f), A_rref, den)
@pytest.mark.parametrize('name, A, A_rref, den', RREF_EXAMPLES)
def test_dm_sparse_rref_den_keep_domain_CD(name, A, A_rref, den):
A = A.to_sparse()
A_rref_f, den_f, pivots_f = A.rref_den(keep_domain=False, method='CD')
A_rref_f = A_rref_f.to_field() / den_f
_check_divide((A_rref_f, pivots_f), A_rref, den)
@pytest.mark.parametrize('name, A, A_rref, den', RREF_EXAMPLES)
def test_dm_sparse_rref_den_keep_domain_GJ(name, A, A_rref, den):
A = A.to_sparse()
A_rref_f, den_f, pivots_f = A.rref_den(keep_domain=False, method='GJ')
A_rref_f = A_rref_f.to_field() / den_f
_check_divide((A_rref_f, pivots_f), A_rref, den)
@pytest.mark.parametrize('name, A, A_rref, den', RREF_EXAMPLES)
def test_ddm_rref_den(name, A, A_rref, den):
A = A.to_ddm()
_check_cancel(A.rref_den(), A_rref, den)
@pytest.mark.parametrize('name, A, A_rref, den', RREF_EXAMPLES)
def test_sdm_rref_den(name, A, A_rref, den):
A = A.to_sdm()
_check_cancel(A.rref_den(), A_rref, den)
@pytest.mark.parametrize('name, A, A_rref, den', RREF_EXAMPLES)
def test_ddm_rref(name, A, A_rref, den):
A = A.to_field().to_ddm()
_check_divide(A.rref(), A_rref, den)
@pytest.mark.parametrize('name, A, A_rref, den', RREF_EXAMPLES)
def test_sdm_rref(name, A, A_rref, den):
A = A.to_field().to_sdm()
_check_divide(A.rref(), A_rref, den)
@pytest.mark.parametrize('name, A, A_rref, den', RREF_EXAMPLES)
def test_ddm_irref(name, A, A_rref, den):
A = A.to_field().to_ddm().copy()
pivots_found = ddm_irref(A)
_check_divide((A, pivots_found), A_rref, den)
@pytest.mark.parametrize('name, A, A_rref, den', RREF_EXAMPLES)
def test_ddm_irref_den(name, A, A_rref, den):
A = A.to_ddm().copy()
(den_found, pivots_found) = ddm_irref_den(A, A.domain)
result = (A, den_found, pivots_found)
_check_cancel(result, A_rref, den)
@pytest.mark.parametrize('name, A, A_rref, den', RREF_EXAMPLES)
def test_sparse_sdm_rref(name, A, A_rref, den):
A = A.to_field().to_sdm()
_check_divide(sdm_irref(A)[:2], A_rref, den)
@pytest.mark.parametrize('name, A, A_rref, den', RREF_EXAMPLES)
def test_sparse_sdm_rref_den(name, A, A_rref, den):
A = A.to_sdm().copy()
K = A.domain
_check_cancel(sdm_rref_den(A, K), A_rref, den)

View File

@ -0,0 +1,428 @@
"""
Tests for the basic functionality of the SDM class.
"""
from itertools import product
from sympy.core.singleton import S
from sympy.external.gmpy import GROUND_TYPES
from sympy.testing.pytest import raises
from sympy.polys.domains import QQ, ZZ, EXRAW
from sympy.polys.matrices.sdm import SDM
from sympy.polys.matrices.ddm import DDM
from sympy.polys.matrices.exceptions import (DMBadInputError, DMDomainError,
DMShapeError)
def test_SDM():
A = SDM({0:{0:ZZ(1)}}, (2, 2), ZZ)
assert A.domain == ZZ
assert A.shape == (2, 2)
assert dict(A) == {0:{0:ZZ(1)}}
raises(DMBadInputError, lambda: SDM({5:{1:ZZ(0)}}, (2, 2), ZZ))
raises(DMBadInputError, lambda: SDM({0:{5:ZZ(0)}}, (2, 2), ZZ))
def test_DDM_str():
sdm = SDM({0:{0:ZZ(1)}, 1:{1:ZZ(1)}}, (2, 2), ZZ)
assert str(sdm) == '{0: {0: 1}, 1: {1: 1}}'
if GROUND_TYPES == 'gmpy': # pragma: no cover
assert repr(sdm) == 'SDM({0: {0: mpz(1)}, 1: {1: mpz(1)}}, (2, 2), ZZ)'
else: # pragma: no cover
assert repr(sdm) == 'SDM({0: {0: 1}, 1: {1: 1}}, (2, 2), ZZ)'
def test_SDM_new():
A = SDM({0:{0:ZZ(1)}}, (2, 2), ZZ)
B = A.new({}, (2, 2), ZZ)
assert B == SDM({}, (2, 2), ZZ)
def test_SDM_copy():
A = SDM({0:{0:ZZ(1)}}, (2, 2), ZZ)
B = A.copy()
assert A == B
A[0][0] = ZZ(2)
assert A != B
def test_SDM_from_list():
A = SDM.from_list([[ZZ(0), ZZ(1)], [ZZ(1), ZZ(0)]], (2, 2), ZZ)
assert A == SDM({0:{1:ZZ(1)}, 1:{0:ZZ(1)}}, (2, 2), ZZ)
raises(DMBadInputError, lambda: SDM.from_list([[ZZ(0)], [ZZ(0), ZZ(1)]], (2, 2), ZZ))
raises(DMBadInputError, lambda: SDM.from_list([[ZZ(0), ZZ(1)]], (2, 2), ZZ))
def test_SDM_to_list():
A = SDM({0:{1: ZZ(1)}}, (2, 2), ZZ)
assert A.to_list() == [[ZZ(0), ZZ(1)], [ZZ(0), ZZ(0)]]
A = SDM({}, (0, 2), ZZ)
assert A.to_list() == []
A = SDM({}, (2, 0), ZZ)
assert A.to_list() == [[], []]
def test_SDM_to_list_flat():
A = SDM({0:{1: ZZ(1)}}, (2, 2), ZZ)
assert A.to_list_flat() == [ZZ(0), ZZ(1), ZZ(0), ZZ(0)]
def test_SDM_to_dok():
A = SDM({0:{1: ZZ(1)}}, (2, 2), ZZ)
assert A.to_dok() == {(0, 1): ZZ(1)}
def test_SDM_from_ddm():
A = DDM([[ZZ(1), ZZ(0)], [ZZ(1), ZZ(0)]], (2, 2), ZZ)
B = SDM.from_ddm(A)
assert B.domain == ZZ
assert B.shape == (2, 2)
assert dict(B) == {0:{0:ZZ(1)}, 1:{0:ZZ(1)}}
def test_SDM_to_ddm():
A = SDM({0:{1: ZZ(1)}}, (2, 2), ZZ)
B = DDM([[ZZ(0), ZZ(1)], [ZZ(0), ZZ(0)]], (2, 2), ZZ)
assert A.to_ddm() == B
def test_SDM_to_sdm():
A = SDM({0:{1: ZZ(1)}}, (2, 2), ZZ)
assert A.to_sdm() == A
def test_SDM_getitem():
A = SDM({0:{1:ZZ(1)}}, (2, 2), ZZ)
assert A.getitem(0, 0) == ZZ.zero
assert A.getitem(0, 1) == ZZ.one
assert A.getitem(1, 0) == ZZ.zero
assert A.getitem(-2, -2) == ZZ.zero
assert A.getitem(-2, -1) == ZZ.one
assert A.getitem(-1, -2) == ZZ.zero
raises(IndexError, lambda: A.getitem(2, 0))
raises(IndexError, lambda: A.getitem(0, 2))
def test_SDM_setitem():
A = SDM({0:{1:ZZ(1)}}, (2, 2), ZZ)
A.setitem(0, 0, ZZ(1))
assert A == SDM({0:{0:ZZ(1), 1:ZZ(1)}}, (2, 2), ZZ)
A.setitem(1, 0, ZZ(1))
assert A == SDM({0:{0:ZZ(1), 1:ZZ(1)}, 1:{0:ZZ(1)}}, (2, 2), ZZ)
A.setitem(1, 0, ZZ(0))
assert A == SDM({0:{0:ZZ(1), 1:ZZ(1)}}, (2, 2), ZZ)
# Repeat the above test so that this time the row is empty
A.setitem(1, 0, ZZ(0))
assert A == SDM({0:{0:ZZ(1), 1:ZZ(1)}}, (2, 2), ZZ)
A.setitem(0, 0, ZZ(0))
assert A == SDM({0:{1:ZZ(1)}}, (2, 2), ZZ)
# This time the row is there but column is empty
A.setitem(0, 0, ZZ(0))
assert A == SDM({0:{1:ZZ(1)}}, (2, 2), ZZ)
raises(IndexError, lambda: A.setitem(2, 0, ZZ(1)))
raises(IndexError, lambda: A.setitem(0, 2, ZZ(1)))
def test_SDM_extract_slice():
A = SDM({0:{0:ZZ(1), 1:ZZ(2)}, 1:{0:ZZ(3), 1:ZZ(4)}}, (2, 2), ZZ)
B = A.extract_slice(slice(1, 2), slice(1, 2))
assert B == SDM({0:{0:ZZ(4)}}, (1, 1), ZZ)
def test_SDM_extract():
A = SDM({0:{0:ZZ(1), 1:ZZ(2)}, 1:{0:ZZ(3), 1:ZZ(4)}}, (2, 2), ZZ)
B = A.extract([1], [1])
assert B == SDM({0:{0:ZZ(4)}}, (1, 1), ZZ)
B = A.extract([1, 0], [1, 0])
assert B == SDM({0:{0:ZZ(4), 1:ZZ(3)}, 1:{0:ZZ(2), 1:ZZ(1)}}, (2, 2), ZZ)
B = A.extract([1, 1], [1, 1])
assert B == SDM({0:{0:ZZ(4), 1:ZZ(4)}, 1:{0:ZZ(4), 1:ZZ(4)}}, (2, 2), ZZ)
B = A.extract([-1], [-1])
assert B == SDM({0:{0:ZZ(4)}}, (1, 1), ZZ)
A = SDM({}, (2, 2), ZZ)
B = A.extract([0, 1, 0], [0, 0])
assert B == SDM({}, (3, 2), ZZ)
A = SDM({0:{0:ZZ(1), 1:ZZ(2)}, 1:{0:ZZ(3), 1:ZZ(4)}}, (2, 2), ZZ)
assert A.extract([], []) == SDM.zeros((0, 0), ZZ)
assert A.extract([1], []) == SDM.zeros((1, 0), ZZ)
assert A.extract([], [1]) == SDM.zeros((0, 1), ZZ)
raises(IndexError, lambda: A.extract([2], [0]))
raises(IndexError, lambda: A.extract([0], [2]))
raises(IndexError, lambda: A.extract([-3], [0]))
raises(IndexError, lambda: A.extract([0], [-3]))
def test_SDM_zeros():
A = SDM.zeros((2, 2), ZZ)
assert A.domain == ZZ
assert A.shape == (2, 2)
assert dict(A) == {}
def test_SDM_ones():
A = SDM.ones((1, 2), QQ)
assert A.domain == QQ
assert A.shape == (1, 2)
assert dict(A) == {0:{0:QQ(1), 1:QQ(1)}}
def test_SDM_eye():
A = SDM.eye((2, 2), ZZ)
assert A.domain == ZZ
assert A.shape == (2, 2)
assert dict(A) == {0:{0:ZZ(1)}, 1:{1:ZZ(1)}}
def test_SDM_diag():
A = SDM.diag([ZZ(1), ZZ(2)], ZZ, (2, 3))
assert A == SDM({0:{0:ZZ(1)}, 1:{1:ZZ(2)}}, (2, 3), ZZ)
def test_SDM_transpose():
A = SDM({0:{0:ZZ(1), 1:ZZ(2)}, 1:{0:ZZ(3), 1:ZZ(4)}}, (2, 2), ZZ)
B = SDM({0:{0:ZZ(1), 1:ZZ(3)}, 1:{0:ZZ(2), 1:ZZ(4)}}, (2, 2), ZZ)
assert A.transpose() == B
A = SDM({0:{1:ZZ(2)}}, (2, 2), ZZ)
B = SDM({1:{0:ZZ(2)}}, (2, 2), ZZ)
assert A.transpose() == B
A = SDM({0:{1:ZZ(2)}}, (1, 2), ZZ)
B = SDM({1:{0:ZZ(2)}}, (2, 1), ZZ)
assert A.transpose() == B
def test_SDM_mul():
A = SDM({0:{0:ZZ(2)}}, (2, 2), ZZ)
B = SDM({0:{0:ZZ(4)}}, (2, 2), ZZ)
assert A*ZZ(2) == B
assert ZZ(2)*A == B
raises(TypeError, lambda: A*QQ(1, 2))
raises(TypeError, lambda: QQ(1, 2)*A)
def test_SDM_mul_elementwise():
A = SDM({0:{0:ZZ(2), 1:ZZ(2)}}, (2, 2), ZZ)
B = SDM({0:{0:ZZ(4)}, 1:{0:ZZ(3)}}, (2, 2), ZZ)
C = SDM({0:{0:ZZ(8)}}, (2, 2), ZZ)
assert A.mul_elementwise(B) == C
assert B.mul_elementwise(A) == C
Aq = A.convert_to(QQ)
A1 = SDM({0:{0:ZZ(1)}}, (1, 1), ZZ)
raises(DMDomainError, lambda: Aq.mul_elementwise(B))
raises(DMShapeError, lambda: A1.mul_elementwise(B))
def test_SDM_matmul():
A = SDM({0:{0:ZZ(2)}}, (2, 2), ZZ)
B = SDM({0:{0:ZZ(4)}}, (2, 2), ZZ)
assert A.matmul(A) == A*A == B
C = SDM({0:{0:ZZ(2)}}, (2, 2), QQ)
raises(DMDomainError, lambda: A.matmul(C))
A = SDM({0:{0:ZZ(1), 1:ZZ(2)}, 1:{0:ZZ(3), 1:ZZ(4)}}, (2, 2), ZZ)
B = SDM({0:{0:ZZ(7), 1:ZZ(10)}, 1:{0:ZZ(15), 1:ZZ(22)}}, (2, 2), ZZ)
assert A.matmul(A) == A*A == B
A22 = SDM({0:{0:ZZ(4)}}, (2, 2), ZZ)
A32 = SDM({0:{0:ZZ(2)}}, (3, 2), ZZ)
A23 = SDM({0:{0:ZZ(4)}}, (2, 3), ZZ)
A33 = SDM({0:{0:ZZ(8)}}, (3, 3), ZZ)
A22 = SDM({0:{0:ZZ(8)}}, (2, 2), ZZ)
assert A32.matmul(A23) == A33
assert A23.matmul(A32) == A22
# XXX: @ not supported by SDM...
#assert A32.matmul(A23) == A32 @ A23 == A33
#assert A23.matmul(A32) == A23 @ A32 == A22
#raises(DMShapeError, lambda: A23 @ A22)
raises(DMShapeError, lambda: A23.matmul(A22))
A = SDM({0: {0: ZZ(-1), 1: ZZ(1)}}, (1, 2), ZZ)
B = SDM({0: {0: ZZ(-1)}, 1: {0: ZZ(-1)}}, (2, 1), ZZ)
assert A.matmul(B) == A*B == SDM({}, (1, 1), ZZ)
def test_matmul_exraw():
def dm(d):
result = {}
for i, row in d.items():
row = {j:val for j, val in row.items() if val}
if row:
result[i] = row
return SDM(result, (2, 2), EXRAW)
values = [S.NegativeInfinity, S.NegativeOne, S.Zero, S.One, S.Infinity]
for a, b, c, d in product(*[values]*4):
Ad = dm({0: {0:a, 1:b}, 1: {0:c, 1:d}})
Ad2 = dm({0: {0:a*a + b*c, 1:a*b + b*d}, 1:{0:c*a + d*c, 1: c*b + d*d}})
assert Ad * Ad == Ad2
def test_SDM_add():
A = SDM({0:{1:ZZ(1)}, 1:{0:ZZ(2), 1:ZZ(3)}}, (2, 2), ZZ)
B = SDM({0:{0:ZZ(1)}, 1:{0:ZZ(-2), 1:ZZ(3)}}, (2, 2), ZZ)
C = SDM({0:{0:ZZ(1), 1:ZZ(1)}, 1:{1:ZZ(6)}}, (2, 2), ZZ)
assert A.add(B) == B.add(A) == A + B == B + A == C
A = SDM({0:{1:ZZ(1)}}, (2, 2), ZZ)
B = SDM({0:{0:ZZ(1)}, 1:{0:ZZ(-2), 1:ZZ(3)}}, (2, 2), ZZ)
C = SDM({0:{0:ZZ(1), 1:ZZ(1)}, 1:{0:ZZ(-2), 1:ZZ(3)}}, (2, 2), ZZ)
assert A.add(B) == B.add(A) == A + B == B + A == C
raises(TypeError, lambda: A + [])
def test_SDM_sub():
A = SDM({0:{1:ZZ(1)}, 1:{0:ZZ(2), 1:ZZ(3)}}, (2, 2), ZZ)
B = SDM({0:{0:ZZ(1)}, 1:{0:ZZ(-2), 1:ZZ(3)}}, (2, 2), ZZ)
C = SDM({0:{0:ZZ(-1), 1:ZZ(1)}, 1:{0:ZZ(4)}}, (2, 2), ZZ)
assert A.sub(B) == A - B == C
raises(TypeError, lambda: A - [])
def test_SDM_neg():
A = SDM({0:{1:ZZ(1)}, 1:{0:ZZ(2), 1:ZZ(3)}}, (2, 2), ZZ)
B = SDM({0:{1:ZZ(-1)}, 1:{0:ZZ(-2), 1:ZZ(-3)}}, (2, 2), ZZ)
assert A.neg() == -A == B
def test_SDM_convert_to():
A = SDM({0:{1:ZZ(1)}, 1:{0:ZZ(2), 1:ZZ(3)}}, (2, 2), ZZ)
B = SDM({0:{1:QQ(1)}, 1:{0:QQ(2), 1:QQ(3)}}, (2, 2), QQ)
C = A.convert_to(QQ)
assert C == B
assert C.domain == QQ
D = A.convert_to(ZZ)
assert D == A
assert D.domain == ZZ
def test_SDM_hstack():
A = SDM({0:{1:ZZ(1)}}, (2, 2), ZZ)
B = SDM({1:{1:ZZ(1)}}, (2, 2), ZZ)
AA = SDM({0:{1:ZZ(1), 3:ZZ(1)}}, (2, 4), ZZ)
AB = SDM({0:{1:ZZ(1)}, 1:{3:ZZ(1)}}, (2, 4), ZZ)
assert SDM.hstack(A) == A
assert SDM.hstack(A, A) == AA
assert SDM.hstack(A, B) == AB
def test_SDM_vstack():
A = SDM({0:{1:ZZ(1)}}, (2, 2), ZZ)
B = SDM({1:{1:ZZ(1)}}, (2, 2), ZZ)
AA = SDM({0:{1:ZZ(1)}, 2:{1:ZZ(1)}}, (4, 2), ZZ)
AB = SDM({0:{1:ZZ(1)}, 3:{1:ZZ(1)}}, (4, 2), ZZ)
assert SDM.vstack(A) == A
assert SDM.vstack(A, A) == AA
assert SDM.vstack(A, B) == AB
def test_SDM_applyfunc():
A = SDM({0:{1:ZZ(1)}}, (2, 2), ZZ)
B = SDM({0:{1:ZZ(2)}}, (2, 2), ZZ)
assert A.applyfunc(lambda x: 2*x, ZZ) == B
def test_SDM_inv():
A = SDM({0:{0:QQ(1), 1:QQ(2)}, 1:{0:QQ(3), 1:QQ(4)}}, (2, 2), QQ)
B = SDM({0:{0:QQ(-2), 1:QQ(1)}, 1:{0:QQ(3, 2), 1:QQ(-1, 2)}}, (2, 2), QQ)
assert A.inv() == B
def test_SDM_det():
A = SDM({0:{0:QQ(1), 1:QQ(2)}, 1:{0:QQ(3), 1:QQ(4)}}, (2, 2), QQ)
assert A.det() == QQ(-2)
def test_SDM_lu():
A = SDM({0:{0:QQ(1), 1:QQ(2)}, 1:{0:QQ(3), 1:QQ(4)}}, (2, 2), QQ)
L = SDM({0:{0:QQ(1)}, 1:{0:QQ(3), 1:QQ(1)}}, (2, 2), QQ)
#U = SDM({0:{0:QQ(1), 1:QQ(2)}, 1:{0:QQ(3), 1:QQ(-2)}}, (2, 2), QQ)
#swaps = []
# This doesn't quite work. U has some nonzero elements in the lower part.
#assert A.lu() == (L, U, swaps)
assert A.lu()[0] == L
def test_SDM_lu_solve():
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)
x = SDM({1:{0:QQ(1, 2)}}, (2, 1), QQ)
assert A.matmul(x) == b
assert A.lu_solve(b) == x
def test_SDM_charpoly():
A = SDM({0:{0:ZZ(1), 1:ZZ(2)}, 1:{0:ZZ(3), 1:ZZ(4)}}, (2, 2), ZZ)
assert A.charpoly() == [ZZ(1), ZZ(-5), ZZ(-2)]
def test_SDM_nullspace():
# More tests are in test_nullspace.py
A = SDM({0:{0:QQ(1), 1:QQ(1)}}, (2, 2), QQ)
assert A.nullspace()[0] == SDM({0:{0:QQ(-1), 1:QQ(1)}}, (1, 2), QQ)
def test_SDM_rref():
# More tests are in test_rref.py
A = SDM({0:{0:QQ(1), 1:QQ(2)},
1:{0:QQ(3), 1:QQ(4)}}, (2, 2), QQ)
A_rref = SDM({0:{0:QQ(1)}, 1:{1:QQ(1)}}, (2, 2), QQ)
assert A.rref() == (A_rref, [0, 1])
A = SDM({0: {0: QQ(1), 1: QQ(2), 2: QQ(2)},
1: {0: QQ(3), 2: QQ(4)}}, (2, 3), ZZ)
A_rref = SDM({0: {0: QQ(1,1), 2: QQ(4,3)},
1: {1: QQ(1,1), 2: QQ(1,3)}}, (2, 3), QQ)
assert A.rref() == (A_rref, [0, 1])
def test_SDM_particular():
A = SDM({0:{0:QQ(1)}}, (2, 2), QQ)
Apart = SDM.zeros((1, 2), QQ)
assert A.particular() == Apart
def test_SDM_is_zero_matrix():
A = SDM({0: {0: QQ(1)}}, (2, 2), QQ)
Azero = SDM.zeros((1, 2), QQ)
assert A.is_zero_matrix() is False
assert Azero.is_zero_matrix() is True
def test_SDM_is_upper():
A = SDM({0: {0: QQ(1), 1: QQ(2), 2: QQ(3), 3: QQ(4)},
1: {1: QQ(5), 2: QQ(6), 3: QQ(7)},
2: {2: QQ(8), 3: QQ(9)}}, (3, 4), QQ)
B = SDM({0: {0: QQ(1), 1: QQ(2), 2: QQ(3), 3: QQ(4)},
1: {1: QQ(5), 2: QQ(6), 3: QQ(7)},
2: {1: QQ(7), 2: QQ(8), 3: QQ(9)}}, (3, 4), QQ)
assert A.is_upper() is True
assert B.is_upper() is False
def test_SDM_is_lower():
A = SDM({0: {0: QQ(1), 1: QQ(2), 2: QQ(3), 3: QQ(4)},
1: {1: QQ(5), 2: QQ(6), 3: QQ(7)},
2: {2: QQ(8), 3: QQ(9)}}, (3, 4), QQ
).transpose()
B = SDM({0: {0: QQ(1), 1: QQ(2), 2: QQ(3), 3: QQ(4)},
1: {1: QQ(5), 2: QQ(6), 3: QQ(7)},
2: {1: QQ(7), 2: QQ(8), 3: QQ(9)}}, (3, 4), QQ
).transpose()
assert A.is_lower() is True
assert B.is_lower() is False

View File

@ -0,0 +1,864 @@
#
# Test basic features of DDM, SDM and DFM.
#
# These three types are supposed to be interchangeable, so we should use the
# same tests for all of them for the most part.
#
# The tests here cover the basic part of the inerface that the three types
# should expose and that DomainMatrix should mostly rely on.
#
# More in-depth tests of the heavier algorithms like rref etc should go in
# their own test files.
#
# Any new methods added to the DDM, SDM or DFM classes should be tested here
# and added to all classes.
#
from sympy.external.gmpy import GROUND_TYPES
from sympy import ZZ, QQ, GF, ZZ_I, symbols
from sympy.polys.matrices.exceptions import (
DMBadInputError,
DMDomainError,
DMNonSquareMatrixError,
DMNonInvertibleMatrixError,
DMShapeError,
)
from sympy.polys.matrices.domainmatrix import DM, DomainMatrix, DDM, SDM, DFM
from sympy.testing.pytest import raises, skip
import pytest
def test_XXM_constructors():
"""Test the DDM, etc constructors."""
lol = [
[ZZ(1), ZZ(2)],
[ZZ(3), ZZ(4)],
[ZZ(5), ZZ(6)],
]
dod = {
0: {0: ZZ(1), 1: ZZ(2)},
1: {0: ZZ(3), 1: ZZ(4)},
2: {0: ZZ(5), 1: ZZ(6)},
}
lol_0x0 = []
lol_0x2 = []
lol_2x0 = [[], []]
dod_0x0 = {}
dod_0x2 = {}
dod_2x0 = {}
lol_bad = [
[ZZ(1), ZZ(2)],
[ZZ(3), ZZ(4)],
[ZZ(5), ZZ(6), ZZ(7)],
]
dod_bad = {
0: {0: ZZ(1), 1: ZZ(2)},
1: {0: ZZ(3), 1: ZZ(4)},
2: {0: ZZ(5), 1: ZZ(6), 2: ZZ(7)},
}
XDM_dense = [DDM]
XDM_sparse = [SDM]
if GROUND_TYPES == 'flint':
XDM_dense.append(DFM)
for XDM in XDM_dense:
A = XDM(lol, (3, 2), ZZ)
assert A.rows == 3
assert A.cols == 2
assert A.domain == ZZ
assert A.shape == (3, 2)
if XDM is not DFM:
assert ZZ.of_type(A[0][0]) is True
else:
assert ZZ.of_type(A.rep[0, 0]) is True
Adm = DomainMatrix(lol, (3, 2), ZZ)
if XDM is DFM:
assert Adm.rep == A
assert Adm.rep.to_ddm() != A
elif GROUND_TYPES == 'flint':
assert Adm.rep.to_ddm() == A
assert Adm.rep != A
else:
assert Adm.rep == A
assert Adm.rep.to_ddm() == A
assert XDM(lol_0x0, (0, 0), ZZ).shape == (0, 0)
assert XDM(lol_0x2, (0, 2), ZZ).shape == (0, 2)
assert XDM(lol_2x0, (2, 0), ZZ).shape == (2, 0)
raises(DMBadInputError, lambda: XDM(lol, (2, 3), ZZ))
raises(DMBadInputError, lambda: XDM(lol_bad, (3, 2), ZZ))
raises(DMBadInputError, lambda: XDM(dod, (3, 2), ZZ))
for XDM in XDM_sparse:
A = XDM(dod, (3, 2), ZZ)
assert A.rows == 3
assert A.cols == 2
assert A.domain == ZZ
assert A.shape == (3, 2)
assert ZZ.of_type(A[0][0]) is True
assert DomainMatrix(dod, (3, 2), ZZ).rep == A
assert XDM(dod_0x0, (0, 0), ZZ).shape == (0, 0)
assert XDM(dod_0x2, (0, 2), ZZ).shape == (0, 2)
assert XDM(dod_2x0, (2, 0), ZZ).shape == (2, 0)
raises(DMBadInputError, lambda: XDM(dod, (2, 3), ZZ))
raises(DMBadInputError, lambda: XDM(lol, (3, 2), ZZ))
raises(DMBadInputError, lambda: XDM(dod_bad, (3, 2), ZZ))
raises(DMBadInputError, lambda: DomainMatrix(lol, (2, 3), ZZ))
raises(DMBadInputError, lambda: DomainMatrix(lol_bad, (3, 2), ZZ))
raises(DMBadInputError, lambda: DomainMatrix(dod_bad, (3, 2), ZZ))
def test_XXM_eq():
"""Test equality for DDM, SDM, DFM and DomainMatrix."""
lol1 = [[ZZ(1), ZZ(2)], [ZZ(3), ZZ(4)]]
dod1 = {0: {0: ZZ(1), 1: ZZ(2)}, 1: {0: ZZ(3), 1: ZZ(4)}}
lol2 = [[ZZ(1), ZZ(2)], [ZZ(3), ZZ(5)]]
dod2 = {0: {0: ZZ(1), 1: ZZ(2)}, 1: {0: ZZ(3), 1: ZZ(5)}}
A1_ddm = DDM(lol1, (2, 2), ZZ)
A1_sdm = SDM(dod1, (2, 2), ZZ)
A1_dm_d = DomainMatrix(lol1, (2, 2), ZZ)
A1_dm_s = DomainMatrix(dod1, (2, 2), ZZ)
A2_ddm = DDM(lol2, (2, 2), ZZ)
A2_sdm = SDM(dod2, (2, 2), ZZ)
A2_dm_d = DomainMatrix(lol2, (2, 2), ZZ)
A2_dm_s = DomainMatrix(dod2, (2, 2), ZZ)
A1_all = [A1_ddm, A1_sdm, A1_dm_d, A1_dm_s]
A2_all = [A2_ddm, A2_sdm, A2_dm_d, A2_dm_s]
if GROUND_TYPES == 'flint':
A1_dfm = DFM([[1, 2], [3, 4]], (2, 2), ZZ)
A2_dfm = DFM([[1, 2], [3, 5]], (2, 2), ZZ)
A1_all.append(A1_dfm)
A2_all.append(A2_dfm)
for n, An in enumerate(A1_all):
for m, Am in enumerate(A1_all):
if n == m:
assert (An == Am) is True
assert (An != Am) is False
else:
assert (An == Am) is False
assert (An != Am) is True
for n, An in enumerate(A2_all):
for m, Am in enumerate(A2_all):
if n == m:
assert (An == Am) is True
assert (An != Am) is False
else:
assert (An == Am) is False
assert (An != Am) is True
for n, A1 in enumerate(A1_all):
for m, A2 in enumerate(A2_all):
assert (A1 == A2) is False
assert (A1 != A2) is True
def test_to_XXM():
"""Test to_ddm etc. for DDM, SDM, DFM and DomainMatrix."""
lol = [[ZZ(1), ZZ(2)], [ZZ(3), ZZ(4)]]
dod = {0: {0: ZZ(1), 1: ZZ(2)}, 1: {0: ZZ(3), 1: ZZ(4)}}
A_ddm = DDM(lol, (2, 2), ZZ)
A_sdm = SDM(dod, (2, 2), ZZ)
A_dm_d = DomainMatrix(lol, (2, 2), ZZ)
A_dm_s = DomainMatrix(dod, (2, 2), ZZ)
A_all = [A_ddm, A_sdm, A_dm_d, A_dm_s]
if GROUND_TYPES == 'flint':
A_dfm = DFM(lol, (2, 2), ZZ)
A_all.append(A_dfm)
for A in A_all:
assert A.to_ddm() == A_ddm
assert A.to_sdm() == A_sdm
if GROUND_TYPES != 'flint':
raises(NotImplementedError, lambda: A.to_dfm())
assert A.to_dfm_or_ddm() == A_ddm
# Add e.g. DDM.to_DM()?
# assert A.to_DM() == A_dm
if GROUND_TYPES == 'flint':
for A in A_all:
assert A.to_dfm() == A_dfm
for K in [ZZ, QQ, GF(5), ZZ_I]:
if isinstance(A, DFM) and not DFM._supports_domain(K):
raises(NotImplementedError, lambda: A.convert_to(K))
else:
A_K = A.convert_to(K)
if DFM._supports_domain(K):
A_dfm_K = A_dfm.convert_to(K)
assert A_K.to_dfm() == A_dfm_K
assert A_K.to_dfm_or_ddm() == A_dfm_K
else:
raises(NotImplementedError, lambda: A_K.to_dfm())
assert A_K.to_dfm_or_ddm() == A_ddm.convert_to(K)
def test_DFM_domains():
"""Test which domains are supported by DFM."""
x, y = symbols('x, y')
if GROUND_TYPES in ('python', 'gmpy'):
supported = []
flint_funcs = {}
not_supported = [ZZ, QQ, GF(5), QQ[x], QQ[x,y]]
elif GROUND_TYPES == 'flint':
import flint
supported = [ZZ, QQ]
flint_funcs = {
ZZ: flint.fmpz_mat,
QQ: flint.fmpq_mat,
}
not_supported = [
# This could be supported but not yet implemented in SymPy:
GF(5),
# Other domains could be supported but not implemented as matrices
# in python-flint:
QQ[x],
QQ[x,y],
QQ.frac_field(x,y),
# Others would potentially never be supported by python-flint:
ZZ_I,
]
else:
assert False, "Unknown GROUND_TYPES: %s" % GROUND_TYPES
for domain in supported:
assert DFM._supports_domain(domain) is True
assert DFM._get_flint_func(domain) == flint_funcs[domain]
for domain in not_supported:
assert DFM._supports_domain(domain) is False
raises(NotImplementedError, lambda: DFM._get_flint_func(domain))
def _DM(lol, typ, K):
"""Make a DM of type typ over K from lol."""
A = DM(lol, K)
if typ == 'DDM':
return A.to_ddm()
elif typ == 'SDM':
return A.to_sdm()
elif typ == 'DFM':
if GROUND_TYPES != 'flint':
skip("DFM not supported in this ground type")
return A.to_dfm()
else:
assert False, "Unknown type %s" % typ
def _DMZ(lol, typ):
"""Make a DM of type typ over ZZ from lol."""
return _DM(lol, typ, ZZ)
def _DMQ(lol, typ):
"""Make a DM of type typ over QQ from lol."""
return _DM(lol, typ, QQ)
def DM_ddm(lol, K):
"""Make a DDM over K from lol."""
return _DM(lol, 'DDM', K)
def DM_sdm(lol, K):
"""Make a SDM over K from lol."""
return _DM(lol, 'SDM', K)
def DM_dfm(lol, K):
"""Make a DFM over K from lol."""
return _DM(lol, 'DFM', K)
def DMZ_ddm(lol):
"""Make a DDM from lol."""
return _DMZ(lol, 'DDM')
def DMZ_sdm(lol):
"""Make a SDM from lol."""
return _DMZ(lol, 'SDM')
def DMZ_dfm(lol):
"""Make a DFM from lol."""
return _DMZ(lol, 'DFM')
def DMQ_ddm(lol):
"""Make a DDM from lol."""
return _DMQ(lol, 'DDM')
def DMQ_sdm(lol):
"""Make a SDM from lol."""
return _DMQ(lol, 'SDM')
def DMQ_dfm(lol):
"""Make a DFM from lol."""
return _DMQ(lol, 'DFM')
DM_all = [DM_ddm, DM_sdm, DM_dfm]
DMZ_all = [DMZ_ddm, DMZ_sdm, DMZ_dfm]
DMQ_all = [DMQ_ddm, DMQ_sdm, DMQ_dfm]
@pytest.mark.parametrize('DM', DMZ_all)
def test_XDM_getitem(DM):
"""Test getitem for DDM, etc."""
lol = [[0, 1], [2, 0]]
A = DM(lol)
m, n = A.shape
indices = [-3, -2, -1, 0, 1, 2]
for i in indices:
for j in indices:
if -2 <= i < m and -2 <= j < n:
assert A.getitem(i, j) == ZZ(lol[i][j])
else:
raises(IndexError, lambda: A.getitem(i, j))
@pytest.mark.parametrize('DM', DMZ_all)
def test_XDM_setitem(DM):
"""Test setitem for DDM, etc."""
A = DM([[0, 1, 2], [3, 4, 5]])
A.setitem(0, 0, ZZ(6))
assert A == DM([[6, 1, 2], [3, 4, 5]])
A.setitem(0, 1, ZZ(7))
assert A == DM([[6, 7, 2], [3, 4, 5]])
A.setitem(0, 2, ZZ(8))
assert A == DM([[6, 7, 8], [3, 4, 5]])
A.setitem(0, -1, ZZ(9))
assert A == DM([[6, 7, 9], [3, 4, 5]])
A.setitem(0, -2, ZZ(10))
assert A == DM([[6, 10, 9], [3, 4, 5]])
A.setitem(0, -3, ZZ(11))
assert A == DM([[11, 10, 9], [3, 4, 5]])
raises(IndexError, lambda: A.setitem(0, 3, ZZ(12)))
raises(IndexError, lambda: A.setitem(0, -4, ZZ(13)))
A.setitem(1, 0, ZZ(14))
assert A == DM([[11, 10, 9], [14, 4, 5]])
A.setitem(1, 1, ZZ(15))
assert A == DM([[11, 10, 9], [14, 15, 5]])
A.setitem(-1, 1, ZZ(16))
assert A == DM([[11, 10, 9], [14, 16, 5]])
A.setitem(-2, 1, ZZ(17))
assert A == DM([[11, 17, 9], [14, 16, 5]])
raises(IndexError, lambda: A.setitem(2, 0, ZZ(18)))
raises(IndexError, lambda: A.setitem(-3, 0, ZZ(19)))
A.setitem(1, 2, ZZ(0))
assert A == DM([[11, 17, 9], [14, 16, 0]])
A.setitem(1, -2, ZZ(0))
assert A == DM([[11, 17, 9], [14, 0, 0]])
A.setitem(1, -3, ZZ(0))
assert A == DM([[11, 17, 9], [0, 0, 0]])
A.setitem(0, 0, ZZ(0))
assert A == DM([[0, 17, 9], [0, 0, 0]])
A.setitem(0, -1, ZZ(0))
assert A == DM([[0, 17, 0], [0, 0, 0]])
A.setitem(0, 0, ZZ(0))
assert A == DM([[0, 17, 0], [0, 0, 0]])
A.setitem(0, -2, ZZ(0))
assert A == DM([[0, 0, 0], [0, 0, 0]])
A.setitem(0, -3, ZZ(1))
assert A == DM([[1, 0, 0], [0, 0, 0]])
class _Sliced:
def __getitem__(self, item):
return item
_slice = _Sliced()
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_extract_slice(DM):
A = DM([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
assert A.extract_slice(*_slice[:,:]) == A
assert A.extract_slice(*_slice[1:,:]) == DM([[4, 5, 6], [7, 8, 9]])
assert A.extract_slice(*_slice[1:,1:]) == DM([[5, 6], [8, 9]])
assert A.extract_slice(*_slice[1:,:-1]) == DM([[4, 5], [7, 8]])
assert A.extract_slice(*_slice[1:,:-1:2]) == DM([[4], [7]])
assert A.extract_slice(*_slice[:,::2]) == DM([[1, 3], [4, 6], [7, 9]])
assert A.extract_slice(*_slice[::2,:]) == DM([[1, 2, 3], [7, 8, 9]])
assert A.extract_slice(*_slice[::2,::2]) == DM([[1, 3], [7, 9]])
assert A.extract_slice(*_slice[::2,::-2]) == DM([[3, 1], [9, 7]])
assert A.extract_slice(*_slice[::-2,::2]) == DM([[7, 9], [1, 3]])
assert A.extract_slice(*_slice[::-2,::-2]) == DM([[9, 7], [3, 1]])
assert A.extract_slice(*_slice[:,::-1]) == DM([[3, 2, 1], [6, 5, 4], [9, 8, 7]])
assert A.extract_slice(*_slice[::-1,:]) == DM([[7, 8, 9], [4, 5, 6], [1, 2, 3]])
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_extract(DM):
A = DM([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
assert A.extract([0, 1, 2], [0, 1, 2]) == A
assert A.extract([1, 2], [1, 2]) == DM([[5, 6], [8, 9]])
assert A.extract([1, 2], [0, 1]) == DM([[4, 5], [7, 8]])
assert A.extract([1, 2], [0, 2]) == DM([[4, 6], [7, 9]])
assert A.extract([1, 2], [0]) == DM([[4], [7]])
assert A.extract([1, 2], []) == DM([[1]]).zeros((2, 0), ZZ)
assert A.extract([], [0, 1, 2]) == DM([[1]]).zeros((0, 3), ZZ)
raises(IndexError, lambda: A.extract([1, 2], [0, 3]))
raises(IndexError, lambda: A.extract([1, 2], [0, -4]))
raises(IndexError, lambda: A.extract([3, 1], [0, 1]))
raises(IndexError, lambda: A.extract([-4, 2], [3, 1]))
B = DM([[0, 0, 0], [0, 0, 0], [0, 0, 0]])
assert B.extract([1, 2], [1, 2]) == DM([[0, 0], [0, 0]])
def test_XXM_str():
A = DomainMatrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]], (3, 3), ZZ)
assert str(A) == \
'DomainMatrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]], (3, 3), ZZ)'
assert str(A.to_ddm()) == \
'[[1, 2, 3], [4, 5, 6], [7, 8, 9]]'
assert str(A.to_sdm()) == \
'{0: {0: 1, 1: 2, 2: 3}, 1: {0: 4, 1: 5, 2: 6}, 2: {0: 7, 1: 8, 2: 9}}'
assert repr(A) == \
'DomainMatrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]], (3, 3), ZZ)'
assert repr(A.to_ddm()) == \
'DDM([[1, 2, 3], [4, 5, 6], [7, 8, 9]], (3, 3), ZZ)'
assert repr(A.to_sdm()) == \
'SDM({0: {0: 1, 1: 2, 2: 3}, 1: {0: 4, 1: 5, 2: 6}, 2: {0: 7, 1: 8, 2: 9}}, (3, 3), ZZ)'
B = DomainMatrix({0: {0: ZZ(1), 1: ZZ(2)}, 1: {0: ZZ(3)}}, (2, 2), ZZ)
assert str(B) == \
'DomainMatrix({0: {0: 1, 1: 2}, 1: {0: 3}}, (2, 2), ZZ)'
assert str(B.to_ddm()) == \
'[[1, 2], [3, 0]]'
assert str(B.to_sdm()) == \
'{0: {0: 1, 1: 2}, 1: {0: 3}}'
assert repr(B) == \
'DomainMatrix({0: {0: 1, 1: 2}, 1: {0: 3}}, (2, 2), ZZ)'
if GROUND_TYPES != 'gmpy':
assert repr(B.to_ddm()) == \
'DDM([[1, 2], [3, 0]], (2, 2), ZZ)'
assert repr(B.to_sdm()) == \
'SDM({0: {0: 1, 1: 2}, 1: {0: 3}}, (2, 2), ZZ)'
else:
assert repr(B.to_ddm()) == \
'DDM([[mpz(1), mpz(2)], [mpz(3), mpz(0)]], (2, 2), ZZ)'
assert repr(B.to_sdm()) == \
'SDM({0: {0: mpz(1), 1: mpz(2)}, 1: {0: mpz(3)}}, (2, 2), ZZ)'
if GROUND_TYPES == 'flint':
assert str(A.to_dfm()) == \
'[[1, 2, 3], [4, 5, 6], [7, 8, 9]]'
assert str(B.to_dfm()) == \
'[[1, 2], [3, 0]]'
assert repr(A.to_dfm()) == \
'DFM([[1, 2, 3], [4, 5, 6], [7, 8, 9]], (3, 3), ZZ)'
assert repr(B.to_dfm()) == \
'DFM([[1, 2], [3, 0]], (2, 2), ZZ)'
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_from_list(DM):
T = type(DM([[0]]))
lol = [[1, 2, 4], [4, 5, 6]]
lol_ZZ = [[ZZ(1), ZZ(2), ZZ(4)], [ZZ(4), ZZ(5), ZZ(6)]]
lol_ZZ_bad = [[ZZ(1), ZZ(2), ZZ(4)], [ZZ(4), ZZ(5), ZZ(6), ZZ(7)]]
assert T.from_list(lol_ZZ, (2, 3), ZZ) == DM(lol)
raises(DMBadInputError, lambda: T.from_list(lol_ZZ_bad, (3, 2), ZZ))
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_to_list(DM):
lol = [[1, 2, 4], [4, 5, 6]]
assert DM(lol).to_list() == [[ZZ(1), ZZ(2), ZZ(4)], [ZZ(4), ZZ(5), ZZ(6)]]
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_to_list_flat(DM):
lol = [[1, 2, 4], [4, 5, 6]]
assert DM(lol).to_list_flat() == [ZZ(1), ZZ(2), ZZ(4), ZZ(4), ZZ(5), ZZ(6)]
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_from_list_flat(DM):
T = type(DM([[0]]))
flat = [ZZ(1), ZZ(2), ZZ(4), ZZ(4), ZZ(5), ZZ(6)]
assert T.from_list_flat(flat, (2, 3), ZZ) == DM([[1, 2, 4], [4, 5, 6]])
raises(DMBadInputError, lambda: T.from_list_flat(flat, (3, 3), ZZ))
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_to_flat_nz(DM):
M = DM([[1, 2, 0], [0, 0, 0], [0, 0, 3]])
elements = [ZZ(1), ZZ(2), ZZ(3)]
indices = ((0, 0), (0, 1), (2, 2))
assert M.to_flat_nz() == (elements, (indices, M.shape))
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_from_flat_nz(DM):
T = type(DM([[0]]))
elements = [ZZ(1), ZZ(2), ZZ(3)]
indices = ((0, 0), (0, 1), (2, 2))
data = (indices, (3, 3))
result = DM([[1, 2, 0], [0, 0, 0], [0, 0, 3]])
assert T.from_flat_nz(elements, data, ZZ) == result
raises(DMBadInputError, lambda: T.from_flat_nz(elements, (indices, (2, 3)), ZZ))
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_to_dod(DM):
dod = {0: {0: ZZ(1), 2: ZZ(4)}, 1: {0: ZZ(4), 1: ZZ(5), 2: ZZ(6)}}
assert DM([[1, 0, 4], [4, 5, 6]]).to_dod() == dod
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_from_dod(DM):
T = type(DM([[0]]))
dod = {0: {0: ZZ(1), 2: ZZ(4)}, 1: {0: ZZ(4), 1: ZZ(5), 2: ZZ(6)}}
assert T.from_dod(dod, (2, 3), ZZ) == DM([[1, 0, 4], [4, 5, 6]])
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_to_dok(DM):
dod = {(0, 0): ZZ(1), (0, 2): ZZ(4),
(1, 0): ZZ(4), (1, 1): ZZ(5), (1, 2): ZZ(6)}
assert DM([[1, 0, 4], [4, 5, 6]]).to_dok() == dod
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_from_dok(DM):
T = type(DM([[0]]))
dod = {(0, 0): ZZ(1), (0, 2): ZZ(4),
(1, 0): ZZ(4), (1, 1): ZZ(5), (1, 2): ZZ(6)}
assert T.from_dok(dod, (2, 3), ZZ) == DM([[1, 0, 4], [4, 5, 6]])
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_iter_values(DM):
values = [ZZ(1), ZZ(4), ZZ(4), ZZ(5), ZZ(6)]
assert sorted(DM([[1, 0, 4], [4, 5, 6]]).iter_values()) == values
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_iter_items(DM):
items = [((0, 0), ZZ(1)), ((0, 2), ZZ(4)),
((1, 0), ZZ(4)), ((1, 1), ZZ(5)), ((1, 2), ZZ(6))]
assert sorted(DM([[1, 0, 4], [4, 5, 6]]).iter_items()) == items
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_from_ddm(DM):
T = type(DM([[0]]))
ddm = DDM([[1, 2, 4], [4, 5, 6]], (2, 3), ZZ)
assert T.from_ddm(ddm) == DM([[1, 2, 4], [4, 5, 6]])
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_zeros(DM):
T = type(DM([[0]]))
assert T.zeros((2, 3), ZZ) == DM([[0, 0, 0], [0, 0, 0]])
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_ones(DM):
T = type(DM([[0]]))
assert T.ones((2, 3), ZZ) == DM([[1, 1, 1], [1, 1, 1]])
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_eye(DM):
T = type(DM([[0]]))
assert T.eye(3, ZZ) == DM([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
assert T.eye((3, 2), ZZ) == DM([[1, 0], [0, 1], [0, 0]])
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_diag(DM):
T = type(DM([[0]]))
assert T.diag([1, 2, 3], ZZ) == DM([[1, 0, 0], [0, 2, 0], [0, 0, 3]])
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_transpose(DM):
A = DM([[1, 2, 3], [4, 5, 6]])
assert A.transpose() == DM([[1, 4], [2, 5], [3, 6]])
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_add(DM):
A = DM([[1, 2, 3], [4, 5, 6]])
B = DM([[1, 2, 3], [4, 5, 6]])
C = DM([[2, 4, 6], [8, 10, 12]])
assert A.add(B) == C
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_sub(DM):
A = DM([[1, 2, 3], [4, 5, 6]])
B = DM([[1, 2, 3], [4, 5, 6]])
C = DM([[0, 0, 0], [0, 0, 0]])
assert A.sub(B) == C
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_mul(DM):
A = DM([[1, 2, 3], [4, 5, 6]])
b = ZZ(2)
assert A.mul(b) == DM([[2, 4, 6], [8, 10, 12]])
assert A.rmul(b) == DM([[2, 4, 6], [8, 10, 12]])
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_matmul(DM):
A = DM([[1, 2, 3], [4, 5, 6]])
B = DM([[1, 2], [3, 4], [5, 6]])
C = DM([[22, 28], [49, 64]])
assert A.matmul(B) == C
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_mul_elementwise(DM):
A = DM([[1, 2, 3], [4, 5, 6]])
B = DM([[1, 2, 3], [4, 5, 6]])
C = DM([[1, 4, 9], [16, 25, 36]])
assert A.mul_elementwise(B) == C
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_neg(DM):
A = DM([[1, 2, 3], [4, 5, 6]])
C = DM([[-1, -2, -3], [-4, -5, -6]])
assert A.neg() == C
@pytest.mark.parametrize('DM', DM_all)
def test_XXM_convert_to(DM):
A = DM([[1, 2, 3], [4, 5, 6]], ZZ)
B = DM([[1, 2, 3], [4, 5, 6]], QQ)
assert A.convert_to(QQ) == B
assert B.convert_to(ZZ) == A
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_scc(DM):
A = DM([
[0, 1, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0],
[0, 0, 0, 1, 0, 1],
[0, 0, 0, 0, 1, 0],
[0, 0, 0, 1, 0, 1]])
assert A.scc() == [[0, 1], [2], [3, 5], [4]]
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_hstack(DM):
A = DM([[1, 2, 3], [4, 5, 6]])
B = DM([[7, 8], [9, 10]])
C = DM([[1, 2, 3, 7, 8], [4, 5, 6, 9, 10]])
ABC = DM([[1, 2, 3, 7, 8, 1, 2, 3, 7, 8],
[4, 5, 6, 9, 10, 4, 5, 6, 9, 10]])
assert A.hstack(B) == C
assert A.hstack(B, C) == ABC
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_vstack(DM):
A = DM([[1, 2, 3], [4, 5, 6]])
B = DM([[7, 8, 9]])
C = DM([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
ABC = DM([[1, 2, 3], [4, 5, 6], [7, 8, 9], [1, 2, 3], [4, 5, 6], [7, 8, 9]])
assert A.vstack(B) == C
assert A.vstack(B, C) == ABC
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_applyfunc(DM):
A = DM([[1, 2, 3], [4, 5, 6]])
B = DM([[2, 4, 6], [8, 10, 12]])
assert A.applyfunc(lambda x: 2*x, ZZ) == B
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_is_upper(DM):
assert DM([[1, 2, 3], [0, 5, 6]]).is_upper() is True
assert DM([[1, 2, 3], [4, 5, 6]]).is_upper() is False
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_is_lower(DM):
assert DM([[1, 0, 0], [4, 5, 0]]).is_lower() is True
assert DM([[1, 2, 3], [4, 5, 6]]).is_lower() is False
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_is_diagonal(DM):
assert DM([[1, 0, 0], [0, 5, 0]]).is_diagonal() is True
assert DM([[1, 2, 3], [4, 5, 6]]).is_diagonal() is False
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_diagonal(DM):
assert DM([[1, 0, 0], [0, 5, 0]]).diagonal() == [1, 5]
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_is_zero_matrix(DM):
assert DM([[0, 0, 0], [0, 0, 0]]).is_zero_matrix() is True
assert DM([[1, 0, 0], [0, 0, 0]]).is_zero_matrix() is False
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_det_ZZ(DM):
assert DM([[1, 2, 3], [4, 5, 6], [7, 8, 9]]).det() == 0
assert DM([[1, 2, 3], [4, 5, 6], [7, 8, 10]]).det() == -3
@pytest.mark.parametrize('DM', DMQ_all)
def test_XXM_det_QQ(DM):
dM1 = DM([[(1,2), (2,3)], [(3,4), (4,5)]])
assert dM1.det() == QQ(-1,10)
@pytest.mark.parametrize('DM', DMQ_all)
def test_XXM_inv_QQ(DM):
dM1 = DM([[(1,2), (2,3)], [(3,4), (4,5)]])
dM2 = DM([[(-8,1), (20,3)], [(15,2), (-5,1)]])
assert dM1.inv() == dM2
assert dM1.matmul(dM2) == DM([[1, 0], [0, 1]])
dM3 = DM([[(1,2), (2,3)], [(1,4), (1,3)]])
raises(DMNonInvertibleMatrixError, lambda: dM3.inv())
dM4 = DM([[(1,2), (2,3), (3,4)], [(1,4), (1,3), (1,2)]])
raises(DMNonSquareMatrixError, lambda: dM4.inv())
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_inv_ZZ(DM):
dM1 = DM([[1, 2, 3], [4, 5, 6], [7, 8, 10]])
# XXX: Maybe this should return a DM over QQ instead?
# XXX: Handle unimodular matrices?
raises(DMDomainError, lambda: dM1.inv())
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_charpoly_ZZ(DM):
dM1 = DM([[1, 2, 3], [4, 5, 6], [7, 8, 10]])
assert dM1.charpoly() == [1, -16, -12, 3]
@pytest.mark.parametrize('DM', DMQ_all)
def test_XXM_charpoly_QQ(DM):
dM1 = DM([[(1,2), (2,3)], [(3,4), (4,5)]])
assert dM1.charpoly() == [QQ(1,1), QQ(-13,10), QQ(-1,10)]
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_lu_solve_ZZ(DM):
dM1 = DM([[1, 2, 3], [4, 5, 6], [7, 8, 10]])
dM2 = DM([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
raises(DMDomainError, lambda: dM1.lu_solve(dM2))
@pytest.mark.parametrize('DM', DMQ_all)
def test_XXM_lu_solve_QQ(DM):
dM1 = DM([[1, 2, 3], [4, 5, 6], [7, 8, 10]])
dM2 = DM([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
dM3 = DM([[(-2,3),(-4,3),(1,1)],[(-2,3),(11,3),(-2,1)],[(1,1),(-2,1),(1,1)]])
assert dM1.lu_solve(dM2) == dM3 == dM1.inv()
dM4 = DM([[1, 2, 3], [4, 5, 6]])
dM5 = DM([[1, 0], [0, 1], [0, 0]])
raises(DMShapeError, lambda: dM4.lu_solve(dM5))
@pytest.mark.parametrize('DM', DMQ_all)
def test_XXM_nullspace_QQ(DM):
dM1 = DM([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
# XXX: Change the signature to just return the nullspace. Possibly
# returning the rank or nullity makes sense but the list of nonpivots is
# not useful.
assert dM1.nullspace() == (DM([[1, -2, 1]]), [2])
@pytest.mark.parametrize('DM', DMZ_all)
def test_XXM_lll(DM):
M = DM([[1, 2, 3], [4, 5, 20]])
M_lll = DM([[1, 2, 3], [-1, -5, 5]])
T = DM([[1, 0], [-5, 1]])
assert M.lll() == M_lll
assert M.lll_transform() == (M_lll, T)
assert T.matmul(M) == M_lll