Source code for pisa.utils.matrix
"""
Utilities for performing some not-so-common matrix tasks.
"""
import numpy as np
import scipy.linalg as lin
from pisa.utils.log import logging, set_verbosity
__all__ = [
'is_psd',
'fronebius_nearest_psd',
]
__author__ = 'A. Trettin'
__license__ = '''Copyright (c) 2020, The IceCube Collaboration
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.'''
[docs]
def is_psd(A):
"""Test whether a matrix is positive semi-definite.
Test is done via attempted Cholesky decomposition as suggested in [1]_.
Parameters
----------
A : numpy.ndarray
Symmetric matrix
Returns
-------
bool
True if `A` is positive semi-definite, else False
References
----------
.. [1] N.J. Higham, "Computing a nearest symmetric positive semidefinite
matrix" (1988): https://doi.org/10.1016/0024-3795(88)90223-6
"""
# pylint: disable=invalid-name
try:
_ = np.linalg.cholesky(A)
return True
except np.linalg.LinAlgError:
return False
[docs]
def fronebius_nearest_psd(A, return_distance=False):
"""Find the positive semi-definite matrix closest to `A`.
The closeness to `A` is measured by the Fronebius norm. The matrix closest to `A`
by that measure is uniquely defined in [3]_.
Parameters
----------
A : numpy.ndarray
Symmetric matrix
return_distance : bool, optional
Return distance of the input matrix to the approximation as given in
theorem 2.1 in [3]_.
This can be compared to the actual Frobenius norm between the
input and output to verify the calculation.
Returns
-------
X : numpy.ndarray
Positive semi-definite matrix approximating `A`.
Notes
-----
This function is a modification of [1]_, which is a Python adaption of [2]_, which
credits [3]_.
References
----------
.. [1] https://gist.github.com/fasiha/fdb5cec2054e6f1c6ae35476045a0bbd
.. [2] https://www.mathworks.com/matlabcentral/fileexchange/42885-nearestspd
.. [3] N.J. Higham, "Computing a nearest symmetric positive semidefinite
matrix" (1988): https://doi.org/10.1016/0024-3795(88)90223-6
"""
# pylint: disable=invalid-name
assert A.ndim == 2, "input is not a 2D matrix"
B = (A + A.T)/2.
_, H = lin.polar(B)
X = (B + H)/2.
# small numerical errors can make matrices that are not exactly
# symmetric, fix that
X = (X + X.T)/2.
# due to numerics, it's possible that the matrix is _still_ not psd.
# We can fix that iteratively by adding small increments of the identity matrix.
# This part comes from [1].
if not is_psd(X):
spacing = np.spacing(lin.norm(X))
I = np.eye(X.shape[0])
k = 1
while not is_psd(X):
mineig = np.min(np.real(lin.eigvals(X)))
X += I * (-mineig * k**2 + spacing)
k += 1
if return_distance:
C = (A - A.T)/2.
lam = lin.eigvalsh(B)
# pylint doesn't know that numpy.sum takes the "where" argument
# pylint: disable=unexpected-keyword-arg
dist = np.sqrt(np.sum(lam**2, where=lam < 0.) + lin.norm(C, ord='fro')**2)
return X, dist
return X
def check_frob_psd(A):
"""Check approximation of Frobenius-closest PSD on given matrix.
This is not a unit test.
Parameters
----------
A : numpy.ndarray
Symmetric matrix
"""
# pylint: disable=invalid-name
X, xdist = fronebius_nearest_psd(A, return_distance=True)
is_psd_after = is_psd(X)
actual_dist = lin.norm(A - X, ord='fro')
assert is_psd_after, "did not produce PSD matrix"
assert np.isclose(xdist, actual_dist), "actual distance differs from expectation"
def test_matrix_random():
"""Unit test producing a number of random matrices and checking if the
approximated matrix is indeed PSD.
"""
m_test = np.array([[1, -1], [2, 4]])
check_frob_psd(m_test)
for i in range(100):
m_test = np.random.randn(3, 3)
check_frob_psd(m_test)
logging.info('<< PASS : test_matrix_random >>')
if __name__ == '__main__':
set_verbosity(1)
test_matrix_random()