Skip to content

Instantly share code, notes, and snippets.

@fasiha
Last active April 17, 2024 04:28
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save fasiha/fdb5cec2054e6f1c6ae35476045a0bbd to your computer and use it in GitHub Desktop.
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.
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!')
@1313e
Copy link

1313e commented Oct 17, 2017

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.

@fasiha
Copy link
Author

fasiha commented Jan 28, 2018

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