Spaces:
Sleeping
Sleeping
from types import FunctionType | |
from collections import Counter | |
from mpmath import mp, workprec | |
from mpmath.libmp.libmpf import prec_to_dps | |
from sympy.core.sorting import default_sort_key | |
from sympy.core.evalf import DEFAULT_MAXPREC, PrecisionExhausted | |
from sympy.core.logic import fuzzy_and, fuzzy_or | |
from sympy.core.numbers import Float | |
from sympy.core.sympify import _sympify | |
from sympy.functions.elementary.miscellaneous import sqrt | |
from sympy.polys import roots, CRootOf, ZZ, QQ, EX | |
from sympy.polys.matrices import DomainMatrix | |
from sympy.polys.matrices.eigen import dom_eigenvects, dom_eigenvects_to_sympy | |
from sympy.polys.polytools import gcd | |
from .exceptions import MatrixError, NonSquareMatrixError | |
from .determinant import _find_reasonable_pivot | |
from .utilities import _iszero, _simplify | |
__doctest_requires__ = { | |
('_is_indefinite', | |
'_is_negative_definite', | |
'_is_negative_semidefinite', | |
'_is_positive_definite', | |
'_is_positive_semidefinite'): ['matplotlib'], | |
} | |
def _eigenvals_eigenvects_mpmath(M): | |
norm2 = lambda v: mp.sqrt(sum(i**2 for i in v)) | |
v1 = None | |
prec = max(x._prec for x in M.atoms(Float)) | |
eps = 2**-prec | |
while prec < DEFAULT_MAXPREC: | |
with workprec(prec): | |
A = mp.matrix(M.evalf(n=prec_to_dps(prec))) | |
E, ER = mp.eig(A) | |
v2 = norm2([i for e in E for i in (mp.re(e), mp.im(e))]) | |
if v1 is not None and mp.fabs(v1 - v2) < eps: | |
return E, ER | |
v1 = v2 | |
prec *= 2 | |
# we get here because the next step would have taken us | |
# past MAXPREC or because we never took a step; in case | |
# of the latter, we refuse to send back a solution since | |
# it would not have been verified; we also resist taking | |
# a small step to arrive exactly at MAXPREC since then | |
# the two calculations might be artificially close. | |
raise PrecisionExhausted | |
def _eigenvals_mpmath(M, multiple=False): | |
"""Compute eigenvalues using mpmath""" | |
E, _ = _eigenvals_eigenvects_mpmath(M) | |
result = [_sympify(x) for x in E] | |
if multiple: | |
return result | |
return dict(Counter(result)) | |
def _eigenvects_mpmath(M): | |
E, ER = _eigenvals_eigenvects_mpmath(M) | |
result = [] | |
for i in range(M.rows): | |
eigenval = _sympify(E[i]) | |
eigenvect = _sympify(ER[:, i]) | |
result.append((eigenval, 1, [eigenvect])) | |
return result | |
# This function is a candidate for caching if it gets implemented for matrices. | |
def _eigenvals( | |
M, error_when_incomplete=True, *, simplify=False, multiple=False, | |
rational=False, **flags): | |
r"""Compute eigenvalues of the matrix. | |
Parameters | |
========== | |
error_when_incomplete : bool, optional | |
If it is set to ``True``, it will raise an error if not all | |
eigenvalues are computed. This is caused by ``roots`` not returning | |
a full list of eigenvalues. | |
simplify : bool or function, optional | |
If it is set to ``True``, it attempts to return the most | |
simplified form of expressions returned by applying default | |
simplification method in every routine. | |
If it is set to ``False``, it will skip simplification in this | |
particular routine to save computation resources. | |
If a function is passed to, it will attempt to apply | |
the particular function as simplification method. | |
rational : bool, optional | |
If it is set to ``True``, every floating point numbers would be | |
replaced with rationals before computation. It can solve some | |
issues of ``roots`` routine not working well with floats. | |
multiple : bool, optional | |
If it is set to ``True``, the result will be in the form of a | |
list. | |
If it is set to ``False``, the result will be in the form of a | |
dictionary. | |
Returns | |
======= | |
eigs : list or dict | |
Eigenvalues of a matrix. The return format would be specified by | |
the key ``multiple``. | |
Raises | |
====== | |
MatrixError | |
If not enough roots had got computed. | |
NonSquareMatrixError | |
If attempted to compute eigenvalues from a non-square matrix. | |
Examples | |
======== | |
>>> from sympy import Matrix | |
>>> M = Matrix(3, 3, [0, 1, 1, 1, 0, 0, 1, 1, 1]) | |
>>> M.eigenvals() | |
{-1: 1, 0: 1, 2: 1} | |
See Also | |
======== | |
MatrixBase.charpoly | |
eigenvects | |
Notes | |
===== | |
Eigenvalues of a matrix $A$ can be computed by solving a matrix | |
equation $\det(A - \lambda I) = 0$ | |
It's not always possible to return radical solutions for | |
eigenvalues for matrices larger than $4, 4$ shape due to | |
Abel-Ruffini theorem. | |
If there is no radical solution is found for the eigenvalue, | |
it may return eigenvalues in the form of | |
:class:`sympy.polys.rootoftools.ComplexRootOf`. | |
""" | |
if not M: | |
if multiple: | |
return [] | |
return {} | |
if not M.is_square: | |
raise NonSquareMatrixError("{} must be a square matrix.".format(M)) | |
if M._rep.domain not in (ZZ, QQ): | |
# Skip this check for ZZ/QQ because it can be slow | |
if all(x.is_number for x in M) and M.has(Float): | |
return _eigenvals_mpmath(M, multiple=multiple) | |
if rational: | |
from sympy.simplify import nsimplify | |
M = M.applyfunc( | |
lambda x: nsimplify(x, rational=True) if x.has(Float) else x) | |
if multiple: | |
return _eigenvals_list( | |
M, error_when_incomplete=error_when_incomplete, simplify=simplify, | |
**flags) | |
return _eigenvals_dict( | |
M, error_when_incomplete=error_when_incomplete, simplify=simplify, | |
**flags) | |
eigenvals_error_message = \ | |
"It is not always possible to express the eigenvalues of a matrix " + \ | |
"of size 5x5 or higher in radicals. " + \ | |
"We have CRootOf, but domains other than the rationals are not " + \ | |
"currently supported. " + \ | |
"If there are no symbols in the matrix, " + \ | |
"it should still be possible to compute numeric approximations " + \ | |
"of the eigenvalues using " + \ | |
"M.evalf().eigenvals() or M.charpoly().nroots()." | |
def _eigenvals_list( | |
M, error_when_incomplete=True, simplify=False, **flags): | |
iblocks = M.strongly_connected_components() | |
all_eigs = [] | |
is_dom = M._rep.domain in (ZZ, QQ) | |
for b in iblocks: | |
# Fast path for a 1x1 block: | |
if is_dom and len(b) == 1: | |
index = b[0] | |
val = M[index, index] | |
all_eigs.append(val) | |
continue | |
block = M[b, b] | |
if isinstance(simplify, FunctionType): | |
charpoly = block.charpoly(simplify=simplify) | |
else: | |
charpoly = block.charpoly() | |
eigs = roots(charpoly, multiple=True, **flags) | |
if len(eigs) != block.rows: | |
try: | |
eigs = charpoly.all_roots(multiple=True) | |
except NotImplementedError: | |
if error_when_incomplete: | |
raise MatrixError(eigenvals_error_message) | |
else: | |
eigs = [] | |
all_eigs += eigs | |
if not simplify: | |
return all_eigs | |
if not isinstance(simplify, FunctionType): | |
simplify = _simplify | |
return [simplify(value) for value in all_eigs] | |
def _eigenvals_dict( | |
M, error_when_incomplete=True, simplify=False, **flags): | |
iblocks = M.strongly_connected_components() | |
all_eigs = {} | |
is_dom = M._rep.domain in (ZZ, QQ) | |
for b in iblocks: | |
# Fast path for a 1x1 block: | |
if is_dom and len(b) == 1: | |
index = b[0] | |
val = M[index, index] | |
all_eigs[val] = all_eigs.get(val, 0) + 1 | |
continue | |
block = M[b, b] | |
if isinstance(simplify, FunctionType): | |
charpoly = block.charpoly(simplify=simplify) | |
else: | |
charpoly = block.charpoly() | |
eigs = roots(charpoly, multiple=False, **flags) | |
if sum(eigs.values()) != block.rows: | |
try: | |
eigs = dict(charpoly.all_roots(multiple=False)) | |
except NotImplementedError: | |
if error_when_incomplete: | |
raise MatrixError(eigenvals_error_message) | |
else: | |
eigs = {} | |
for k, v in eigs.items(): | |
if k in all_eigs: | |
all_eigs[k] += v | |
else: | |
all_eigs[k] = v | |
if not simplify: | |
return all_eigs | |
if not isinstance(simplify, FunctionType): | |
simplify = _simplify | |
return {simplify(key): value for key, value in all_eigs.items()} | |
def _eigenspace(M, eigenval, iszerofunc=_iszero, simplify=False): | |
"""Get a basis for the eigenspace for a particular eigenvalue""" | |
m = M - M.eye(M.rows) * eigenval | |
ret = m.nullspace(iszerofunc=iszerofunc) | |
# The nullspace for a real eigenvalue should be non-trivial. | |
# If we didn't find an eigenvector, try once more a little harder | |
if len(ret) == 0 and simplify: | |
ret = m.nullspace(iszerofunc=iszerofunc, simplify=True) | |
if len(ret) == 0: | |
raise NotImplementedError( | |
"Can't evaluate eigenvector for eigenvalue {}".format(eigenval)) | |
return ret | |
def _eigenvects_DOM(M, **kwargs): | |
DOM = DomainMatrix.from_Matrix(M, field=True, extension=True) | |
DOM = DOM.to_dense() | |
if DOM.domain != EX: | |
rational, algebraic = dom_eigenvects(DOM) | |
eigenvects = dom_eigenvects_to_sympy( | |
rational, algebraic, M.__class__, **kwargs) | |
eigenvects = sorted(eigenvects, key=lambda x: default_sort_key(x[0])) | |
return eigenvects | |
return None | |
def _eigenvects_sympy(M, iszerofunc, simplify=True, **flags): | |
eigenvals = M.eigenvals(rational=False, **flags) | |
# Make sure that we have all roots in radical form | |
for x in eigenvals: | |
if x.has(CRootOf): | |
raise MatrixError( | |
"Eigenvector computation is not implemented if the matrix have " | |
"eigenvalues in CRootOf form") | |
eigenvals = sorted(eigenvals.items(), key=default_sort_key) | |
ret = [] | |
for val, mult in eigenvals: | |
vects = _eigenspace(M, val, iszerofunc=iszerofunc, simplify=simplify) | |
ret.append((val, mult, vects)) | |
return ret | |
# This functions is a candidate for caching if it gets implemented for matrices. | |
def _eigenvects(M, error_when_incomplete=True, iszerofunc=_iszero, *, chop=False, **flags): | |
"""Compute eigenvectors of the matrix. | |
Parameters | |
========== | |
error_when_incomplete : bool, optional | |
Raise an error when not all eigenvalues are computed. This is | |
caused by ``roots`` not returning a full list of eigenvalues. | |
iszerofunc : function, optional | |
Specifies a zero testing function to be used in ``rref``. | |
Default value is ``_iszero``, which uses SymPy's naive and fast | |
default assumption handler. | |
It can also accept any user-specified zero testing function, if it | |
is formatted as a function which accepts a single symbolic argument | |
and returns ``True`` if it is tested as zero and ``False`` if it | |
is tested as non-zero, and ``None`` if it is undecidable. | |
simplify : bool or function, optional | |
If ``True``, ``as_content_primitive()`` will be used to tidy up | |
normalization artifacts. | |
It will also be used by the ``nullspace`` routine. | |
chop : bool or positive number, optional | |
If the matrix contains any Floats, they will be changed to Rationals | |
for computation purposes, but the answers will be returned after | |
being evaluated with evalf. The ``chop`` flag is passed to ``evalf``. | |
When ``chop=True`` a default precision will be used; a number will | |
be interpreted as the desired level of precision. | |
Returns | |
======= | |
ret : [(eigenval, multiplicity, eigenspace), ...] | |
A ragged list containing tuples of data obtained by ``eigenvals`` | |
and ``nullspace``. | |
``eigenspace`` is a list containing the ``eigenvector`` for each | |
eigenvalue. | |
``eigenvector`` is a vector in the form of a ``Matrix``. e.g. | |
a vector of length 3 is returned as ``Matrix([a_1, a_2, a_3])``. | |
Raises | |
====== | |
NotImplementedError | |
If failed to compute nullspace. | |
Examples | |
======== | |
>>> from sympy import Matrix | |
>>> M = Matrix(3, 3, [0, 1, 1, 1, 0, 0, 1, 1, 1]) | |
>>> M.eigenvects() | |
[(-1, 1, [Matrix([ | |
[-1], | |
[ 1], | |
[ 0]])]), (0, 1, [Matrix([ | |
[ 0], | |
[-1], | |
[ 1]])]), (2, 1, [Matrix([ | |
[2/3], | |
[1/3], | |
[ 1]])])] | |
See Also | |
======== | |
eigenvals | |
MatrixBase.nullspace | |
""" | |
simplify = flags.get('simplify', True) | |
primitive = flags.get('simplify', False) | |
flags.pop('simplify', None) # remove this if it's there | |
flags.pop('multiple', None) # remove this if it's there | |
if not isinstance(simplify, FunctionType): | |
simpfunc = _simplify if simplify else lambda x: x | |
has_floats = M.has(Float) | |
if has_floats: | |
if all(x.is_number for x in M): | |
return _eigenvects_mpmath(M) | |
from sympy.simplify import nsimplify | |
M = M.applyfunc(lambda x: nsimplify(x, rational=True)) | |
ret = _eigenvects_DOM(M) | |
if ret is None: | |
ret = _eigenvects_sympy(M, iszerofunc, simplify=simplify, **flags) | |
if primitive: | |
# if the primitive flag is set, get rid of any common | |
# integer denominators | |
def denom_clean(l): | |
return [(v / gcd(list(v))).applyfunc(simpfunc) for v in l] | |
ret = [(val, mult, denom_clean(es)) for val, mult, es in ret] | |
if has_floats: | |
# if we had floats to start with, turn the eigenvectors to floats | |
ret = [(val.evalf(chop=chop), mult, [v.evalf(chop=chop) for v in es]) | |
for val, mult, es in ret] | |
return ret | |
def _is_diagonalizable_with_eigen(M, reals_only=False): | |
"""See _is_diagonalizable. This function returns the bool along with the | |
eigenvectors to avoid calculating them again in functions like | |
``diagonalize``.""" | |
if not M.is_square: | |
return False, [] | |
eigenvecs = M.eigenvects(simplify=True) | |
for val, mult, basis in eigenvecs: | |
if reals_only and not val.is_real: # if we have a complex eigenvalue | |
return False, eigenvecs | |
if mult != len(basis): # if the geometric multiplicity doesn't equal the algebraic | |
return False, eigenvecs | |
return True, eigenvecs | |
def _is_diagonalizable(M, reals_only=False, **kwargs): | |
"""Returns ``True`` if a matrix is diagonalizable. | |
Parameters | |
========== | |
reals_only : bool, optional | |
If ``True``, it tests whether the matrix can be diagonalized | |
to contain only real numbers on the diagonal. | |
If ``False``, it tests whether the matrix can be diagonalized | |
at all, even with numbers that may not be real. | |
Examples | |
======== | |
Example of a diagonalizable matrix: | |
>>> from sympy import Matrix | |
>>> M = Matrix([[1, 2, 0], [0, 3, 0], [2, -4, 2]]) | |
>>> M.is_diagonalizable() | |
True | |
Example of a non-diagonalizable matrix: | |
>>> M = Matrix([[0, 1], [0, 0]]) | |
>>> M.is_diagonalizable() | |
False | |
Example of a matrix that is diagonalized in terms of non-real entries: | |
>>> M = Matrix([[0, 1], [-1, 0]]) | |
>>> M.is_diagonalizable(reals_only=False) | |
True | |
>>> M.is_diagonalizable(reals_only=True) | |
False | |
See Also | |
======== | |
sympy.matrices.matrixbase.MatrixBase.is_diagonal | |
diagonalize | |
""" | |
if not M.is_square: | |
return False | |
if all(e.is_real for e in M) and M.is_symmetric(): | |
return True | |
if all(e.is_complex for e in M) and M.is_hermitian: | |
return True | |
return _is_diagonalizable_with_eigen(M, reals_only=reals_only)[0] | |
#G&VL, Matrix Computations, Algo 5.4.2 | |
def _householder_vector(x): | |
if not x.cols == 1: | |
raise ValueError("Input must be a column matrix") | |
v = x.copy() | |
v_plus = x.copy() | |
v_minus = x.copy() | |
q = x[0, 0] / abs(x[0, 0]) | |
norm_x = x.norm() | |
v_plus[0, 0] = x[0, 0] + q * norm_x | |
v_minus[0, 0] = x[0, 0] - q * norm_x | |
if x[1:, 0].norm() == 0: | |
bet = 0 | |
v[0, 0] = 1 | |
else: | |
if v_plus.norm() <= v_minus.norm(): | |
v = v_plus | |
else: | |
v = v_minus | |
v = v / v[0] | |
bet = 2 / (v.norm() ** 2) | |
return v, bet | |
def _bidiagonal_decmp_hholder(M): | |
m = M.rows | |
n = M.cols | |
A = M.as_mutable() | |
U, V = A.eye(m), A.eye(n) | |
for i in range(min(m, n)): | |
v, bet = _householder_vector(A[i:, i]) | |
hh_mat = A.eye(m - i) - bet * v * v.H | |
A[i:, i:] = hh_mat * A[i:, i:] | |
temp = A.eye(m) | |
temp[i:, i:] = hh_mat | |
U = U * temp | |
if i + 1 <= n - 2: | |
v, bet = _householder_vector(A[i, i+1:].T) | |
hh_mat = A.eye(n - i - 1) - bet * v * v.H | |
A[i:, i+1:] = A[i:, i+1:] * hh_mat | |
temp = A.eye(n) | |
temp[i+1:, i+1:] = hh_mat | |
V = temp * V | |
return U, A, V | |
def _eval_bidiag_hholder(M): | |
m = M.rows | |
n = M.cols | |
A = M.as_mutable() | |
for i in range(min(m, n)): | |
v, bet = _householder_vector(A[i:, i]) | |
hh_mat = A.eye(m-i) - bet * v * v.H | |
A[i:, i:] = hh_mat * A[i:, i:] | |
if i + 1 <= n - 2: | |
v, bet = _householder_vector(A[i, i+1:].T) | |
hh_mat = A.eye(n - i - 1) - bet * v * v.H | |
A[i:, i+1:] = A[i:, i+1:] * hh_mat | |
return A | |
def _bidiagonal_decomposition(M, upper=True): | |
""" | |
Returns $(U,B,V.H)$ for | |
$$A = UBV^{H}$$ | |
where $A$ is the input matrix, and $B$ is its Bidiagonalized form | |
Note: Bidiagonal Computation can hang for symbolic matrices. | |
Parameters | |
========== | |
upper : bool. Whether to do upper bidiagnalization or lower. | |
True for upper and False for lower. | |
References | |
========== | |
.. [1] Algorithm 5.4.2, Matrix computations by Golub and Van Loan, 4th edition | |
.. [2] Complex Matrix Bidiagonalization, https://github.com/vslobody/Householder-Bidiagonalization | |
""" | |
if not isinstance(upper, bool): | |
raise ValueError("upper must be a boolean") | |
if upper: | |
return _bidiagonal_decmp_hholder(M) | |
X = _bidiagonal_decmp_hholder(M.H) | |
return X[2].H, X[1].H, X[0].H | |
def _bidiagonalize(M, upper=True): | |
""" | |
Returns $B$, the Bidiagonalized form of the input matrix. | |
Note: Bidiagonal Computation can hang for symbolic matrices. | |
Parameters | |
========== | |
upper : bool. Whether to do upper bidiagnalization or lower. | |
True for upper and False for lower. | |
References | |
========== | |
.. [1] Algorithm 5.4.2, Matrix computations by Golub and Van Loan, 4th edition | |
.. [2] Complex Matrix Bidiagonalization : https://github.com/vslobody/Householder-Bidiagonalization | |
""" | |
if not isinstance(upper, bool): | |
raise ValueError("upper must be a boolean") | |
if upper: | |
return _eval_bidiag_hholder(M) | |
return _eval_bidiag_hholder(M.H).H | |
def _diagonalize(M, reals_only=False, sort=False, normalize=False): | |
""" | |
Return (P, D), where D is diagonal and | |
D = P^-1 * M * P | |
where M is current matrix. | |
Parameters | |
========== | |
reals_only : bool. Whether to throw an error if complex numbers are need | |
to diagonalize. (Default: False) | |
sort : bool. Sort the eigenvalues along the diagonal. (Default: False) | |
normalize : bool. If True, normalize the columns of P. (Default: False) | |
Examples | |
======== | |
>>> from sympy import Matrix | |
>>> M = Matrix(3, 3, [1, 2, 0, 0, 3, 0, 2, -4, 2]) | |
>>> M | |
Matrix([ | |
[1, 2, 0], | |
[0, 3, 0], | |
[2, -4, 2]]) | |
>>> (P, D) = M.diagonalize() | |
>>> D | |
Matrix([ | |
[1, 0, 0], | |
[0, 2, 0], | |
[0, 0, 3]]) | |
>>> P | |
Matrix([ | |
[-1, 0, -1], | |
[ 0, 0, -1], | |
[ 2, 1, 2]]) | |
>>> P.inv() * M * P | |
Matrix([ | |
[1, 0, 0], | |
[0, 2, 0], | |
[0, 0, 3]]) | |
See Also | |
======== | |
sympy.matrices.matrixbase.MatrixBase.is_diagonal | |
is_diagonalizable | |
""" | |
if not M.is_square: | |
raise NonSquareMatrixError() | |
is_diagonalizable, eigenvecs = _is_diagonalizable_with_eigen(M, | |
reals_only=reals_only) | |
if not is_diagonalizable: | |
raise MatrixError("Matrix is not diagonalizable") | |
if sort: | |
eigenvecs = sorted(eigenvecs, key=default_sort_key) | |
p_cols, diag = [], [] | |
for val, mult, basis in eigenvecs: | |
diag += [val] * mult | |
p_cols += basis | |
if normalize: | |
p_cols = [v / v.norm() for v in p_cols] | |
return M.hstack(*p_cols), M.diag(*diag) | |
def _fuzzy_positive_definite(M): | |
positive_diagonals = M._has_positive_diagonals() | |
if positive_diagonals is False: | |
return False | |
if positive_diagonals and M.is_strongly_diagonally_dominant: | |
return True | |
return None | |
def _fuzzy_positive_semidefinite(M): | |
nonnegative_diagonals = M._has_nonnegative_diagonals() | |
if nonnegative_diagonals is False: | |
return False | |
if nonnegative_diagonals and M.is_weakly_diagonally_dominant: | |
return True | |
return None | |
def _is_positive_definite(M): | |
if not M.is_hermitian: | |
if not M.is_square: | |
return False | |
M = M + M.H | |
fuzzy = _fuzzy_positive_definite(M) | |
if fuzzy is not None: | |
return fuzzy | |
return _is_positive_definite_GE(M) | |
def _is_positive_semidefinite(M): | |
if not M.is_hermitian: | |
if not M.is_square: | |
return False | |
M = M + M.H | |
fuzzy = _fuzzy_positive_semidefinite(M) | |
if fuzzy is not None: | |
return fuzzy | |
return _is_positive_semidefinite_cholesky(M) | |
def _is_negative_definite(M): | |
return _is_positive_definite(-M) | |
def _is_negative_semidefinite(M): | |
return _is_positive_semidefinite(-M) | |
def _is_indefinite(M): | |
if M.is_hermitian: | |
eigen = M.eigenvals() | |
args1 = [x.is_positive for x in eigen.keys()] | |
any_positive = fuzzy_or(args1) | |
args2 = [x.is_negative for x in eigen.keys()] | |
any_negative = fuzzy_or(args2) | |
return fuzzy_and([any_positive, any_negative]) | |
elif M.is_square: | |
return (M + M.H).is_indefinite | |
return False | |
def _is_positive_definite_GE(M): | |
"""A division-free gaussian elimination method for testing | |
positive-definiteness.""" | |
M = M.as_mutable() | |
size = M.rows | |
for i in range(size): | |
is_positive = M[i, i].is_positive | |
if is_positive is not True: | |
return is_positive | |
for j in range(i+1, size): | |
M[j, i+1:] = M[i, i] * M[j, i+1:] - M[j, i] * M[i, i+1:] | |
return True | |
def _is_positive_semidefinite_cholesky(M): | |
"""Uses Cholesky factorization with complete pivoting | |
References | |
========== | |
.. [1] http://eprints.ma.man.ac.uk/1199/1/covered/MIMS_ep2008_116.pdf | |
.. [2] https://www.value-at-risk.net/cholesky-factorization/ | |
""" | |
M = M.as_mutable() | |
for k in range(M.rows): | |
diags = [M[i, i] for i in range(k, M.rows)] | |
pivot, pivot_val, nonzero, _ = _find_reasonable_pivot(diags) | |
if nonzero: | |
return None | |
if pivot is None: | |
for i in range(k+1, M.rows): | |
for j in range(k, M.cols): | |
iszero = M[i, j].is_zero | |
if iszero is None: | |
return None | |
elif iszero is False: | |
return False | |
return True | |
if M[k, k].is_negative or pivot_val.is_negative: | |
return False | |
elif not (M[k, k].is_nonnegative and pivot_val.is_nonnegative): | |
return None | |
if pivot > 0: | |
M.col_swap(k, k+pivot) | |
M.row_swap(k, k+pivot) | |
M[k, k] = sqrt(M[k, k]) | |
M[k, k+1:] /= M[k, k] | |
M[k+1:, k+1:] -= M[k, k+1:].H * M[k, k+1:] | |
return M[-1, -1].is_nonnegative | |
_doc_positive_definite = \ | |
r"""Finds out the definiteness of a matrix. | |
Explanation | |
=========== | |
A square real matrix $A$ is: | |
- A positive definite matrix if $x^T A x > 0$ | |
for all non-zero real vectors $x$. | |
- A positive semidefinite matrix if $x^T A x \geq 0$ | |
for all non-zero real vectors $x$. | |
- A negative definite matrix if $x^T A x < 0$ | |
for all non-zero real vectors $x$. | |
- A negative semidefinite matrix if $x^T A x \leq 0$ | |
for all non-zero real vectors $x$. | |
- An indefinite matrix if there exists non-zero real vectors | |
$x, y$ with $x^T A x > 0 > y^T A y$. | |
A square complex matrix $A$ is: | |
- A positive definite matrix if $\text{re}(x^H A x) > 0$ | |
for all non-zero complex vectors $x$. | |
- A positive semidefinite matrix if $\text{re}(x^H A x) \geq 0$ | |
for all non-zero complex vectors $x$. | |
- A negative definite matrix if $\text{re}(x^H A x) < 0$ | |
for all non-zero complex vectors $x$. | |
- A negative semidefinite matrix if $\text{re}(x^H A x) \leq 0$ | |
for all non-zero complex vectors $x$. | |
- An indefinite matrix if there exists non-zero complex vectors | |
$x, y$ with $\text{re}(x^H A x) > 0 > \text{re}(y^H A y)$. | |
A matrix need not be symmetric or hermitian to be positive definite. | |
- A real non-symmetric matrix is positive definite if and only if | |
$\frac{A + A^T}{2}$ is positive definite. | |
- A complex non-hermitian matrix is positive definite if and only if | |
$\frac{A + A^H}{2}$ is positive definite. | |
And this extension can apply for all the definitions above. | |
However, for complex cases, you can restrict the definition of | |
$\text{re}(x^H A x) > 0$ to $x^H A x > 0$ and require the matrix | |
to be hermitian. | |
But we do not present this restriction for computation because you | |
can check ``M.is_hermitian`` independently with this and use | |
the same procedure. | |
Examples | |
======== | |
An example of symmetric positive definite matrix: | |
.. plot:: | |
:context: reset | |
:format: doctest | |
:include-source: True | |
>>> from sympy import Matrix, symbols | |
>>> from sympy.plotting import plot3d | |
>>> a, b = symbols('a b') | |
>>> x = Matrix([a, b]) | |
>>> A = Matrix([[1, 0], [0, 1]]) | |
>>> A.is_positive_definite | |
True | |
>>> A.is_positive_semidefinite | |
True | |
>>> p = plot3d((x.T*A*x)[0, 0], (a, -1, 1), (b, -1, 1)) | |
An example of symmetric positive semidefinite matrix: | |
.. plot:: | |
:context: close-figs | |
:format: doctest | |
:include-source: True | |
>>> A = Matrix([[1, -1], [-1, 1]]) | |
>>> A.is_positive_definite | |
False | |
>>> A.is_positive_semidefinite | |
True | |
>>> p = plot3d((x.T*A*x)[0, 0], (a, -1, 1), (b, -1, 1)) | |
An example of symmetric negative definite matrix: | |
.. plot:: | |
:context: close-figs | |
:format: doctest | |
:include-source: True | |
>>> A = Matrix([[-1, 0], [0, -1]]) | |
>>> A.is_negative_definite | |
True | |
>>> A.is_negative_semidefinite | |
True | |
>>> A.is_indefinite | |
False | |
>>> p = plot3d((x.T*A*x)[0, 0], (a, -1, 1), (b, -1, 1)) | |
An example of symmetric indefinite matrix: | |
.. plot:: | |
:context: close-figs | |
:format: doctest | |
:include-source: True | |
>>> A = Matrix([[1, 2], [2, -1]]) | |
>>> A.is_indefinite | |
True | |
>>> p = plot3d((x.T*A*x)[0, 0], (a, -1, 1), (b, -1, 1)) | |
An example of non-symmetric positive definite matrix. | |
.. plot:: | |
:context: close-figs | |
:format: doctest | |
:include-source: True | |
>>> A = Matrix([[1, 2], [-2, 1]]) | |
>>> A.is_positive_definite | |
True | |
>>> A.is_positive_semidefinite | |
True | |
>>> p = plot3d((x.T*A*x)[0, 0], (a, -1, 1), (b, -1, 1)) | |
Notes | |
===== | |
Although some people trivialize the definition of positive definite | |
matrices only for symmetric or hermitian matrices, this restriction | |
is not correct because it does not classify all instances of | |
positive definite matrices from the definition $x^T A x > 0$ or | |
$\text{re}(x^H A x) > 0$. | |
For instance, ``Matrix([[1, 2], [-2, 1]])`` presented in | |
the example above is an example of real positive definite matrix | |
that is not symmetric. | |
However, since the following formula holds true; | |
.. math:: | |
\text{re}(x^H A x) > 0 \iff | |
\text{re}(x^H \frac{A + A^H}{2} x) > 0 | |
We can classify all positive definite matrices that may or may not | |
be symmetric or hermitian by transforming the matrix to | |
$\frac{A + A^T}{2}$ or $\frac{A + A^H}{2}$ | |
(which is guaranteed to be always real symmetric or complex | |
hermitian) and we can defer most of the studies to symmetric or | |
hermitian positive definite matrices. | |
But it is a different problem for the existence of Cholesky | |
decomposition. Because even though a non symmetric or a non | |
hermitian matrix can be positive definite, Cholesky or LDL | |
decomposition does not exist because the decompositions require the | |
matrix to be symmetric or hermitian. | |
References | |
========== | |
.. [1] https://en.wikipedia.org/wiki/Definiteness_of_a_matrix#Eigenvalues | |
.. [2] https://mathworld.wolfram.com/PositiveDefiniteMatrix.html | |
.. [3] Johnson, C. R. "Positive Definite Matrices." Amer. | |
Math. Monthly 77, 259-264 1970. | |
""" | |
_is_positive_definite.__doc__ = _doc_positive_definite | |
_is_positive_semidefinite.__doc__ = _doc_positive_definite | |
_is_negative_definite.__doc__ = _doc_positive_definite | |
_is_negative_semidefinite.__doc__ = _doc_positive_definite | |
_is_indefinite.__doc__ = _doc_positive_definite | |
def _jordan_form(M, calc_transform=True, *, chop=False): | |
"""Return $(P, J)$ where $J$ is a Jordan block | |
matrix and $P$ is a matrix such that $M = P J P^{-1}$ | |
Parameters | |
========== | |
calc_transform : bool | |
If ``False``, then only $J$ is returned. | |
chop : bool | |
All matrices are converted to exact types when computing | |
eigenvalues and eigenvectors. As a result, there may be | |
approximation errors. If ``chop==True``, these errors | |
will be truncated. | |
Examples | |
======== | |
>>> from sympy import Matrix | |
>>> M = Matrix([[ 6, 5, -2, -3], [-3, -1, 3, 3], [ 2, 1, -2, -3], [-1, 1, 5, 5]]) | |
>>> P, J = M.jordan_form() | |
>>> J | |
Matrix([ | |
[2, 1, 0, 0], | |
[0, 2, 0, 0], | |
[0, 0, 2, 1], | |
[0, 0, 0, 2]]) | |
See Also | |
======== | |
jordan_block | |
""" | |
if not M.is_square: | |
raise NonSquareMatrixError("Only square matrices have Jordan forms") | |
mat = M | |
has_floats = M.has(Float) | |
if has_floats: | |
try: | |
max_prec = max(term._prec for term in M.values() if isinstance(term, Float)) | |
except ValueError: | |
# if no term in the matrix is explicitly a Float calling max() | |
# will throw a error so setting max_prec to default value of 53 | |
max_prec = 53 | |
# setting minimum max_dps to 15 to prevent loss of precision in | |
# matrix containing non evaluated expressions | |
max_dps = max(prec_to_dps(max_prec), 15) | |
def restore_floats(*args): | |
"""If ``has_floats`` is `True`, cast all ``args`` as | |
matrices of floats.""" | |
if has_floats: | |
args = [m.evalf(n=max_dps, chop=chop) for m in args] | |
if len(args) == 1: | |
return args[0] | |
return args | |
# cache calculations for some speedup | |
mat_cache = {} | |
def eig_mat(val, pow): | |
"""Cache computations of ``(M - val*I)**pow`` for quick | |
retrieval""" | |
if (val, pow) in mat_cache: | |
return mat_cache[(val, pow)] | |
if (val, pow - 1) in mat_cache: | |
mat_cache[(val, pow)] = mat_cache[(val, pow - 1)].multiply( | |
mat_cache[(val, 1)], dotprodsimp=None) | |
else: | |
mat_cache[(val, pow)] = (mat - val*M.eye(M.rows)).pow(pow) | |
return mat_cache[(val, pow)] | |
# helper functions | |
def nullity_chain(val, algebraic_multiplicity): | |
"""Calculate the sequence [0, nullity(E), nullity(E**2), ...] | |
until it is constant where ``E = M - val*I``""" | |
# mat.rank() is faster than computing the null space, | |
# so use the rank-nullity theorem | |
cols = M.cols | |
ret = [0] | |
nullity = cols - eig_mat(val, 1).rank() | |
i = 2 | |
while nullity != ret[-1]: | |
ret.append(nullity) | |
if nullity == algebraic_multiplicity: | |
break | |
nullity = cols - eig_mat(val, i).rank() | |
i += 1 | |
# Due to issues like #7146 and #15872, SymPy sometimes | |
# gives the wrong rank. In this case, raise an error | |
# instead of returning an incorrect matrix | |
if nullity < ret[-1] or nullity > algebraic_multiplicity: | |
raise MatrixError( | |
"SymPy had encountered an inconsistent " | |
"result while computing Jordan block: " | |
"{}".format(M)) | |
return ret | |
def blocks_from_nullity_chain(d): | |
"""Return a list of the size of each Jordan block. | |
If d_n is the nullity of E**n, then the number | |
of Jordan blocks of size n is | |
2*d_n - d_(n-1) - d_(n+1)""" | |
# d[0] is always the number of columns, so skip past it | |
mid = [2*d[n] - d[n - 1] - d[n + 1] for n in range(1, len(d) - 1)] | |
# d is assumed to plateau with "d[ len(d) ] == d[-1]", so | |
# 2*d_n - d_(n-1) - d_(n+1) == d_n - d_(n-1) | |
end = [d[-1] - d[-2]] if len(d) > 1 else [d[0]] | |
return mid + end | |
def pick_vec(small_basis, big_basis): | |
"""Picks a vector from big_basis that isn't in | |
the subspace spanned by small_basis""" | |
if len(small_basis) == 0: | |
return big_basis[0] | |
for v in big_basis: | |
_, pivots = M.hstack(*(small_basis + [v])).echelon_form( | |
with_pivots=True) | |
if pivots[-1] == len(small_basis): | |
return v | |
# roots doesn't like Floats, so replace them with Rationals | |
if has_floats: | |
from sympy.simplify import nsimplify | |
mat = mat.applyfunc(lambda x: nsimplify(x, rational=True)) | |
# first calculate the jordan block structure | |
eigs = mat.eigenvals() | |
# Make sure that we have all roots in radical form | |
for x in eigs: | |
if x.has(CRootOf): | |
raise MatrixError( | |
"Jordan normal form is not implemented if the matrix have " | |
"eigenvalues in CRootOf form") | |
# most matrices have distinct eigenvalues | |
# and so are diagonalizable. In this case, don't | |
# do extra work! | |
if len(eigs.keys()) == mat.cols: | |
blocks = sorted(eigs.keys(), key=default_sort_key) | |
jordan_mat = mat.diag(*blocks) | |
if not calc_transform: | |
return restore_floats(jordan_mat) | |
jordan_basis = [eig_mat(eig, 1).nullspace()[0] | |
for eig in blocks] | |
basis_mat = mat.hstack(*jordan_basis) | |
return restore_floats(basis_mat, jordan_mat) | |
block_structure = [] | |
for eig in sorted(eigs.keys(), key=default_sort_key): | |
algebraic_multiplicity = eigs[eig] | |
chain = nullity_chain(eig, algebraic_multiplicity) | |
block_sizes = blocks_from_nullity_chain(chain) | |
# if block_sizes = = [a, b, c, ...], then the number of | |
# Jordan blocks of size 1 is a, of size 2 is b, etc. | |
# create an array that has (eig, block_size) with one | |
# entry for each block | |
size_nums = [(i+1, num) for i, num in enumerate(block_sizes)] | |
# we expect larger Jordan blocks to come earlier | |
size_nums.reverse() | |
block_structure.extend( | |
[(eig, size) for size, num in size_nums for _ in range(num)]) | |
jordan_form_size = sum(size for eig, size in block_structure) | |
if jordan_form_size != M.rows: | |
raise MatrixError( | |
"SymPy had encountered an inconsistent result while " | |
"computing Jordan block. : {}".format(M)) | |
blocks = (mat.jordan_block(size=size, eigenvalue=eig) for eig, size in block_structure) | |
jordan_mat = mat.diag(*blocks) | |
if not calc_transform: | |
return restore_floats(jordan_mat) | |
# For each generalized eigenspace, calculate a basis. | |
# We start by looking for a vector in null( (A - eig*I)**n ) | |
# which isn't in null( (A - eig*I)**(n-1) ) where n is | |
# the size of the Jordan block | |
# | |
# Ideally we'd just loop through block_structure and | |
# compute each generalized eigenspace. However, this | |
# causes a lot of unneeded computation. Instead, we | |
# go through the eigenvalues separately, since we know | |
# their generalized eigenspaces must have bases that | |
# are linearly independent. | |
jordan_basis = [] | |
for eig in sorted(eigs.keys(), key=default_sort_key): | |
eig_basis = [] | |
for block_eig, size in block_structure: | |
if block_eig != eig: | |
continue | |
null_big = (eig_mat(eig, size)).nullspace() | |
null_small = (eig_mat(eig, size - 1)).nullspace() | |
# we want to pick something that is in the big basis | |
# and not the small, but also something that is independent | |
# of any other generalized eigenvectors from a different | |
# generalized eigenspace sharing the same eigenvalue. | |
vec = pick_vec(null_small + eig_basis, null_big) | |
new_vecs = [eig_mat(eig, i).multiply(vec, dotprodsimp=None) | |
for i in range(size)] | |
eig_basis.extend(new_vecs) | |
jordan_basis.extend(reversed(new_vecs)) | |
basis_mat = mat.hstack(*jordan_basis) | |
return restore_floats(basis_mat, jordan_mat) | |
def _left_eigenvects(M, **flags): | |
"""Returns left eigenvectors and eigenvalues. | |
This function returns the list of triples (eigenval, multiplicity, | |
basis) for the left eigenvectors. Options are the same as for | |
eigenvects(), i.e. the ``**flags`` arguments gets passed directly to | |
eigenvects(). | |
Examples | |
======== | |
>>> from sympy import Matrix | |
>>> M = Matrix([[0, 1, 1], [1, 0, 0], [1, 1, 1]]) | |
>>> M.eigenvects() | |
[(-1, 1, [Matrix([ | |
[-1], | |
[ 1], | |
[ 0]])]), (0, 1, [Matrix([ | |
[ 0], | |
[-1], | |
[ 1]])]), (2, 1, [Matrix([ | |
[2/3], | |
[1/3], | |
[ 1]])])] | |
>>> M.left_eigenvects() | |
[(-1, 1, [Matrix([[-2, 1, 1]])]), (0, 1, [Matrix([[-1, -1, 1]])]), (2, | |
1, [Matrix([[1, 1, 1]])])] | |
""" | |
eigs = M.transpose().eigenvects(**flags) | |
return [(val, mult, [l.transpose() for l in basis]) for val, mult, basis in eigs] | |
def _singular_values(M): | |
"""Compute the singular values of a Matrix | |
Examples | |
======== | |
>>> from sympy import Matrix, Symbol | |
>>> x = Symbol('x', real=True) | |
>>> M = Matrix([[0, 1, 0], [0, x, 0], [-1, 0, 0]]) | |
>>> M.singular_values() | |
[sqrt(x**2 + 1), 1, 0] | |
See Also | |
======== | |
condition_number | |
""" | |
if M.rows >= M.cols: | |
valmultpairs = M.H.multiply(M).eigenvals() | |
else: | |
valmultpairs = M.multiply(M.H).eigenvals() | |
# Expands result from eigenvals into a simple list | |
vals = [] | |
for k, v in valmultpairs.items(): | |
vals += [sqrt(k)] * v # dangerous! same k in several spots! | |
# Pad with zeros if singular values are computed in reverse way, | |
# to give consistent format. | |
if len(vals) < M.cols: | |
vals += [M.zero] * (M.cols - len(vals)) | |
# sort them in descending order | |
vals.sort(reverse=True, key=default_sort_key) | |
return vals | |