Last active
April 17, 2024 04:28
-
-
Save fasiha/fdb5cec2054e6f1c6ae35476045a0bbd to your computer and use it in GitHub Desktop.
Python/Numpy port of John D’Errico’s implementation (https://www.mathworks.com/matlabcentral/fileexchange/42885-nearestspd) of Higham’s 1988 paper (https://doi.org/10.1016/0024-3795(88)90223-6), including a built-in unit test. License: whatever D’Errico’s license, since this is a port of that.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from numpy import linalg as la | |
import numpy as np | |
def nearestPD(A): | |
"""Find the nearest positive-definite matrix to input | |
A Python/Numpy port of John D'Errico's `nearestSPD` MATLAB code [1], which | |
credits [2]. | |
[1] https://www.mathworks.com/matlabcentral/fileexchange/42885-nearestspd | |
[2] N.J. Higham, "Computing a nearest symmetric positive semidefinite | |
matrix" (1988): https://doi.org/10.1016/0024-3795(88)90223-6 | |
""" | |
B = (A + A.T) / 2 | |
_, s, V = la.svd(B) | |
H = np.dot(V.T, np.dot(np.diag(s), V)) | |
A2 = (B + H) / 2 | |
A3 = (A2 + A2.T) / 2 | |
if isPD(A3): | |
return A3 | |
spacing = np.spacing(la.norm(A)) | |
# The above is different from [1]. It appears that MATLAB's `chol` Cholesky | |
# decomposition will accept matrixes with exactly 0-eigenvalue, whereas | |
# Numpy's will not. So where [1] uses `eps(mineig)` (where `eps` is Matlab | |
# for `np.spacing`), we use the above definition. CAVEAT: our `spacing` | |
# will be much larger than [1]'s `eps(mineig)`, since `mineig` is usually on | |
# the order of 1e-16, and `eps(1e-16)` is on the order of 1e-34, whereas | |
# `spacing` will, for Gaussian random matrixes of small dimension, be on | |
# othe order of 1e-16. In practice, both ways converge, as the unit test | |
# below suggests. | |
I = np.eye(A.shape[0]) | |
k = 1 | |
while not isPD(A3): | |
mineig = np.min(np.real(la.eigvals(A3))) | |
A3 += I * (-mineig * k**2 + spacing) | |
k += 1 | |
return A3 | |
def isPD(B): | |
"""Returns true when input is positive-definite, via Cholesky""" | |
try: | |
_ = la.cholesky(B) | |
return True | |
except la.LinAlgError: | |
return False | |
if __name__ == '__main__': | |
import numpy as np | |
for i in range(10): | |
for j in range(2, 100): | |
A = np.random.randn(j, j) | |
B = nearestPD(A) | |
assert (isPD(B)) | |
print('unit test passed!') |
Since this Python port is a derivative of the original Matlab code by John D'Errico, which is BSD licensed, I release this code also under the BSD license. Go forth and be happy.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is actually a really nice code and the solution to a problem I was having with inverting large matrices that should always be positive-definite, but might not be one due to computational inaccuracies.
Do you allow me to take this code, improve upon it and then make it part of a package that I am building?
Of course referencing where I got the original code from.