import logging
import numpy as np
import numpy.linalg as la
logger = logging.getLogger(__name__)
[docs]
def is_psd(matrix: np.ndarray, tolerance: float = 1e-8) -> bool:
"""Check if a matrix is positive semi-definite (PSD) within a tolerance.
Args:
matrix: Square matrix to test.
tolerance: Numerical tolerance for symmetry and eigenvalues.
Returns:
bool: ``True`` if the matrix is PSD.
"""
if matrix.ndim != 2 or matrix.shape[0] != matrix.shape[1]:
raise ValueError("Input matrix must be a square 2D NumPy array.")
if not np.allclose(matrix, matrix.T, atol=tolerance):
return False
try:
eigenvalues = la.eigvalsh(matrix)
return np.all(eigenvalues >= -tolerance)
except la.LinAlgError:
return False
[docs]
def ensure_psd_matrix(matrix: np.ndarray, jitter: float = 1e-8) -> np.ndarray:
"""Ensure a matrix is PSD by adding jitter to the diagonal if necessary.
Args:
matrix: Square matrix to regularize.
jitter: Diagonal jitter added when PSD fails.
Returns:
np.ndarray: PSD-adjusted matrix.
"""
if matrix.ndim != 2 or matrix.shape[0] != matrix.shape[1]:
raise ValueError("Input matrix must be a square 2D NumPy array.")
matrix = (matrix + matrix.T) / 2.0
if is_psd(matrix, tolerance=jitter):
return matrix
min_eig = 0
try:
eigenvalues = la.eigvalsh(matrix)
min_eig = np.min(eigenvalues)
except la.LinAlgError:
pass
k = max(0, -min_eig) + jitter
psd_matrix = matrix + k * np.eye(matrix.shape[0])
if not is_psd(psd_matrix, tolerance=jitter):
logger.warning(
f"Matrix still not PSD after initial jittering with k={k}. Attempting larger jitter."
)
psd_matrix = matrix + (np.abs(min_eig) + 1e-6) * np.eye(matrix.shape[0])
if not is_psd(psd_matrix, tolerance=jitter):
raise ValueError(
"Failed to make matrix PSD even with increased jitter. Input matrix might be severely ill-conditioned."
)
return psd_matrix
[docs]
def check_weights_sum_to_one(weights: np.ndarray, tolerance: float = 1e-6) -> bool:
"""Check if weights sum approximately to one within a tolerance.
Args:
weights: Weight vector.
tolerance: Absolute tolerance around 1.0.
Returns:
bool: ``True`` if weights sum to one within tolerance.
"""
return np.isclose(np.sum(weights), 1.0, atol=tolerance)
[docs]
def check_non_negativity(array: np.ndarray, tolerance: float = 1e-9) -> bool:
"""Check if all elements in the array are non-negative within a tolerance.
Args:
array: Array to test.
tolerance: Allowed negative tolerance.
Returns:
bool: ``True`` if all entries are non-negative within tolerance.
"""
return np.all(array >= -tolerance)